From 2e73916fa20456af6b737c1208bfa3da0cc679ec Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 9 Jan 2011 15:22:25 -0800 Subject: [PATCH] Initial commit. --- .arcconfig | 7 + .divinerconfig | 2 + .gitignore | 9 + LICENSE | 13 + README | 11 + bin/arc | 1 + externals/README | 2 + externals/pep8/README | 3 + scripts/__init_script__.php | 32 + scripts/arcanist.php | 181 + scripts/phutil_analyzer.php | 364 ++ scripts/phutil_mapper.php | 128 + src/__phutil_library_init__.php | 19 + src/__phutil_library_map__.php | 114 + src/configuration/ArcanistConfiguration.php | 80 + src/configuration/__init__.php | 13 + .../ArcanistDifferentialCommitMessage.php | 93 + ...fferentialCommitMessageParserException.php | 21 + src/differential/commitmessage/__init__.php | 11 + .../ArcanistDifferentialRevisionRef.php | 55 + src/differential/revision/__init__.php | 19 + src/docs/overview.diviner | 30 + ...ArcanistChooseInvalidRevisionException.php | 21 + .../ArcanistChooseNoRevisionsException.php | 21 + src/exception/__init__.php | 20 + .../usage/ArcanistUsageException.php | 21 + src/exception/usage/__init__.php | 10 + .../noeffect/ArcanistNoEffectException.php | 20 + src/exception/usage/noeffect/__init__.php | 12 + .../noengine/ArcanistNoEngineException.php | 20 + src/exception/usage/noengine/__init__.php | 12 + .../userabort/ArcanistUserAbortException.php | 23 + src/exception/usage/userabort/__init__.php | 12 + src/lint/engine/base/ArcanistLintEngine.php | 199 ++ src/lint/engine/base/__init__.php | 17 + src/lint/engine/phutil/PhutilLintEngine.php | 96 + src/lint/engine/phutil/__init__.php | 20 + .../test/UnitTestableArcanistLintEngine.php | 37 + src/lint/engine/test/__init__.php | 12 + .../ArcanistApacheLicenseLinter.php | 99 + src/lint/linter/apachelicense/__init__.php | 13 + src/lint/linter/base/ArcanistLinter.php | 181 + src/lint/linter/base/__init__.php | 12 + .../filename/ArcanistFilenameLinter.php | 50 + src/lint/linter/filename/__init__.php | 21 + .../generated/ArcanistGeneratedLinter.php | 34 + src/lint/linter/generated/__init__.php | 12 + src/lint/linter/pep8/ArcanistPEP8Linter.php | 78 + src/lint/linter/pep8/__init__.php | 17 + .../ArcanistPhutilModuleLinter.php | 478 +++ src/lint/linter/phutilmodule/__init__.php | 19 + src/lint/linter/text/ArcanistTextLinter.php | 184 ++ src/lint/linter/text/__init__.php | 13 + .../linter/xhpast/ArcanistXHPASTLinter.php | 909 +++++ src/lint/linter/xhpast/__init__.php | 17 + .../ArcanistXHPASTLinterTestCase.php | 172 + src/lint/linter/xhpast/__tests__/__init__.php | 19 + .../__tests__/data/array-index.lint-test | 16 + .../__tests__/data/dynamic-define.lint-test | 9 + .../__tests__/data/embedded-tags.lint-test | 5 + .../__tests__/data/exit-expression.lint-test | 8 + .../data/foil-marcels-lexer.lint-test | 81 + .../__tests__/data/hash-comments.lint-test | 34 + .../data/naming-conventions.lint-test | 41 + .../data/no-segfault-on-abstract.lint-test | 4 + .../data/no-segfault-on-exit.lint-test | 3 + .../data/parens-hug-contents.lint-test | 66 + .../__tests__/data/php-tags-bad.lint-test | 14 + .../data/php-tags-echo-form.lint-test | 4 + .../__tests__/data/php-tags-good.lint-test | 4 + .../__tests__/data/php-tags-script.lint-test | 5 + .../__tests__/data/preg-quote.lint-test | 12 + .../single-pass-adjacent-patches.lint-test | 16 + .../space-after-control-keywords.lint-test | 34 + .../data/space-around-operators.lint-test | 59 + .../data/surprising-constructors.lint-test | 8 + .../__tests__/data/syntax-error.lint-test | 4 + .../data/undeclared-variables.lint-test | 175 + .../data/unreasonably-deep-nesting.lint-test | 86 + .../use-of-this-in-static-method.lint-test | 13 + .../data/variable-variables.lint-test | 5 + src/lint/message/ArcanistLintMessage.php | 169 + src/lint/message/__init__.php | 11 + src/lint/patcher/ArcanistLintPatcher.php | 148 + src/lint/patcher/__init__.php | 14 + src/lint/renderer/ArcanistLintRenderer.php | 189 ++ src/lint/renderer/__init__.php | 14 + src/lint/result/ArcanistLintResult.php | 90 + src/lint/result/__init__.php | 12 + src/lint/severity/ArcanistLintSeverity.php | 61 + src/lint/severity/__init__.php | 12 + src/parser/bundle/ArcanistBundle.php | 337 ++ src/parser/bundle/__init__.php | 20 + src/parser/diff/ArcanistDiffParser.php | 815 +++++ src/parser/diff/__init__.php | 17 + .../__tests__/ArcanistDiffParserTestCase.php | 458 +++ src/parser/diff/__tests__/__init__.php | 16 + .../diff/__tests__/data/basic-binary.udiff | 1 + .../basic-missing-both-newlines-plus.udiff | 12 + .../data/basic-missing-both-newlines.udiff | 7 + .../data/basic-missing-new-newline-plus.udiff | 11 + .../data/basic-missing-new-newline.udiff | 6 + .../data/basic-missing-old-newline-plus.udiff | 11 + .../data/basic-missing-old-newline.udiff | 6 + .../data/basic-multi-hunk-content.svndiff | 18 + .../__tests__/data/basic-multi-hunk.udiff | 32 + .../__tests__/data/git-binary-change.gitdiff | 3 + .../diff/__tests__/data/git-commit.gitdiff | 31 + .../diff/__tests__/data/git-copy-plus.gitdiff | 44 + .../diff/__tests__/data/git-copy.gitdiff | 4 + .../__tests__/data/git-delete-file.gitdiff | 19 + .../data/git-filemode-change-only.gitdiff | 14 + .../data/git-filemode-change.gitdiff | 14 + .../data/git-ignore-whitespace-only.gitdiff | 13 + .../__tests__/data/git-merge-header.gitdiff | 19 + .../diff/__tests__/data/git-move-edit.gitdiff | 16 + .../diff/__tests__/data/git-move-plus.gitdiff | 17 + .../diff/__tests__/data/git-move.gitdiff | 4 + .../diff/__tests__/data/git-new-file.gitdiff | 7 + .../__tests__/data/svn-binary-add.svndiff | 10 + .../__tests__/data/svn-binary-diff.svndiff | 4 + .../__tests__/data/svn-empty-file.svndiff | 11 + .../data/svn-ignore-whitespace-only.svndiff | 15 + .../__tests__/data/svn-property-add.svndiff | 18 + .../data/svn-property-delete.svndiff | 6 + .../__tests__/data/svn-property-merge.svndiff | 5 + .../data/svn-property-merged.svndiff | 4 + .../data/svn-property-modify.svndiff | 15 + .../data/svn-property-multiline.svndiff | 12 + src/parser/diff/change/ArcanistDiffChange.php | 222 ++ src/parser/diff/change/__init__.php | 15 + .../changetype/ArcanistDiffChangeType.php | 124 + src/parser/diff/changetype/__init__.php | 21 + src/parser/diff/hunk/ArcanistDiffHunk.php | 188 ++ src/parser/diff/hunk/__init__.php | 19 + .../api/base/ArcanistRepositoryAPI.php | 136 + src/repository/api/base/__init__.php | 15 + src/repository/api/git/ArcanistGitAPI.php | 290 ++ src/repository/api/git/__init__.php | 14 + .../api/subversion/ArcanistSubversionAPI.php | 395 +++ src/repository/api/subversion/__init__.php | 17 + .../phutilmodule/PhutilModuleRequirements.php | 171 + .../parsers/phutilmodule/__init__.php | 19 + .../exception/XHPASTSyntaxErrorException.php | 32 + .../parsers/xhpast/api/exception/__init__.php | 10 + .../xhpast/api/list/XHPASTNodeList.php | 152 + .../parsers/xhpast/api/list/__init__.php | 19 + .../parsers/xhpast/api/node/XHPASTNode.php | 231 ++ .../parsers/xhpast/api/node/__init__.php | 23 + .../parsers/xhpast/api/token/XHPASTToken.php | 102 + .../parsers/xhpast/api/token/__init__.php | 12 + .../parsers/xhpast/api/tree/XHPASTTree.php | 117 + .../parsers/xhpast/api/tree/__init__.php | 17 + .../parsers/xhpast/bin/__init__.php | 21 + .../parsers/xhpast/bin/xhpast_parse.php | 36 + .../parsers/xhpast/constants/__init__.php | 20 + .../parsers/xhpast/constants/parser_nodes.php | 139 + .../xhpast/constants/parser_tokens.php | 172 + .../base/ArcanistBaseUnitTestEngine.php | 50 + src/unit/engine/base/__init__.php | 10 + .../engine/phutil/PhutilUnitTestEngine.php | 105 + src/unit/engine/phutil/__init__.php | 18 + .../testcase/ArcanistPhutilTestCase.php | 91 + src/unit/engine/phutil/testcase/__init__.php | 13 + .../ArcanistPhutilTestTerminatedException.php | 19 + .../phutil/testcase/exception/__init__.php | 10 + src/unit/result/ArcanistUnitTestResult.php | 59 + src/unit/result/__init__.php | 10 + src/workflow/amend/ArcanistAmendWorkflow.php | 127 + src/workflow/amend/__init__.php | 18 + src/workflow/base/ArcanistBaseWorkflow.php | 542 +++ src/workflow/base/__init__.php | 21 + .../commit/ArcanistCommitWorkflow.php | 251 ++ src/workflow/commit/__init__.php | 19 + src/workflow/cover/ArcanistCoverWorkflow.php | 156 + src/workflow/cover/__init__.php | 17 + src/workflow/diff/ArcanistDiffWorkflow.php | 784 +++++ src/workflow/diff/__init__.php | 24 + .../export/ArcanistExportWorkflow.php | 209 ++ src/workflow/export/__init__.php | 17 + .../ArcanistGitHookPreReceiveWorkflow.php | 132 + .../git-hook-pre-receive/__init__.php | 21 + src/workflow/help/ArcanistHelpWorkflow.php | 164 + src/workflow/help/__init__.php | 15 + src/workflow/lint/ArcanistLintWorkflow.php | 226 ++ src/workflow/lint/__init__.php | 27 + src/workflow/list/ArcanistListWorkflow.php | 81 + src/workflow/list/__init__.php | 16 + .../ArcanistMarkCommittedWorkflow.php | 98 + src/workflow/mark-committed/__init__.php | 15 + src/workflow/patch/ArcanistPatchWorkflow.php | 300 ++ src/workflow/patch/__init__.php | 22 + src/workflow/unit/ArcanistUnitWorkflow.php | 123 + src/workflow/unit/__init__.php | 19 + .../ArcanistWorkingCopyIdentity.php | 81 + src/workingcopyidentity/__init__.php | 21 + support/xhpast/Makefile | 46 + support/xhpast/ast.hpp | 101 + support/xhpast/astnode.cpp | 17 + support/xhpast/astnode.hpp | 126 + support/xhpast/generate_nodes.php | 165 + support/xhpast/node_names.hpp | 117 + support/xhpast/parser.y | 2915 +++++++++++++++++ support/xhpast/parser_nodes.php | 123 + support/xhpast/scanner.l | 1085 ++++++ support/xhpast/xhpast.cpp | 150 + 206 files changed, 19132 insertions(+) create mode 100644 .arcconfig create mode 100644 .divinerconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README create mode 120000 bin/arc create mode 100644 externals/README create mode 100644 externals/pep8/README create mode 100644 scripts/__init_script__.php create mode 100755 scripts/arcanist.php create mode 100755 scripts/phutil_analyzer.php create mode 100755 scripts/phutil_mapper.php create mode 100644 src/__phutil_library_init__.php create mode 100644 src/__phutil_library_map__.php create mode 100644 src/configuration/ArcanistConfiguration.php create mode 100644 src/configuration/__init__.php create mode 100644 src/differential/commitmessage/ArcanistDifferentialCommitMessage.php create mode 100644 src/differential/commitmessage/ArcanistDifferentialCommitMessageParserException.php create mode 100644 src/differential/commitmessage/__init__.php create mode 100644 src/differential/revision/ArcanistDifferentialRevisionRef.php create mode 100644 src/differential/revision/__init__.php create mode 100644 src/docs/overview.diviner create mode 100644 src/exception/ArcanistChooseInvalidRevisionException.php create mode 100644 src/exception/ArcanistChooseNoRevisionsException.php create mode 100644 src/exception/__init__.php create mode 100644 src/exception/usage/ArcanistUsageException.php create mode 100644 src/exception/usage/__init__.php create mode 100644 src/exception/usage/noeffect/ArcanistNoEffectException.php create mode 100644 src/exception/usage/noeffect/__init__.php create mode 100644 src/exception/usage/noengine/ArcanistNoEngineException.php create mode 100644 src/exception/usage/noengine/__init__.php create mode 100644 src/exception/usage/userabort/ArcanistUserAbortException.php create mode 100644 src/exception/usage/userabort/__init__.php create mode 100644 src/lint/engine/base/ArcanistLintEngine.php create mode 100644 src/lint/engine/base/__init__.php create mode 100644 src/lint/engine/phutil/PhutilLintEngine.php create mode 100644 src/lint/engine/phutil/__init__.php create mode 100644 src/lint/engine/test/UnitTestableArcanistLintEngine.php create mode 100644 src/lint/engine/test/__init__.php create mode 100644 src/lint/linter/apachelicense/ArcanistApacheLicenseLinter.php create mode 100644 src/lint/linter/apachelicense/__init__.php create mode 100644 src/lint/linter/base/ArcanistLinter.php create mode 100644 src/lint/linter/base/__init__.php create mode 100644 src/lint/linter/filename/ArcanistFilenameLinter.php create mode 100644 src/lint/linter/filename/__init__.php create mode 100644 src/lint/linter/generated/ArcanistGeneratedLinter.php create mode 100644 src/lint/linter/generated/__init__.php create mode 100644 src/lint/linter/pep8/ArcanistPEP8Linter.php create mode 100644 src/lint/linter/pep8/__init__.php create mode 100644 src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php create mode 100644 src/lint/linter/phutilmodule/__init__.php create mode 100644 src/lint/linter/text/ArcanistTextLinter.php create mode 100644 src/lint/linter/text/__init__.php create mode 100644 src/lint/linter/xhpast/ArcanistXHPASTLinter.php create mode 100644 src/lint/linter/xhpast/__init__.php create mode 100644 src/lint/linter/xhpast/__tests__/ArcanistXHPASTLinterTestCase.php create mode 100644 src/lint/linter/xhpast/__tests__/__init__.php create mode 100644 src/lint/linter/xhpast/__tests__/data/array-index.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/dynamic-define.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/embedded-tags.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/exit-expression.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/foil-marcels-lexer.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/hash-comments.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/naming-conventions.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/no-segfault-on-abstract.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/no-segfault-on-exit.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/parens-hug-contents.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/php-tags-bad.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/php-tags-echo-form.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/php-tags-good.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/php-tags-script.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/preg-quote.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/single-pass-adjacent-patches.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/space-after-control-keywords.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/space-around-operators.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/surprising-constructors.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/syntax-error.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/undeclared-variables.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/unreasonably-deep-nesting.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/use-of-this-in-static-method.lint-test create mode 100644 src/lint/linter/xhpast/__tests__/data/variable-variables.lint-test create mode 100644 src/lint/message/ArcanistLintMessage.php create mode 100644 src/lint/message/__init__.php create mode 100644 src/lint/patcher/ArcanistLintPatcher.php create mode 100644 src/lint/patcher/__init__.php create mode 100644 src/lint/renderer/ArcanistLintRenderer.php create mode 100644 src/lint/renderer/__init__.php create mode 100644 src/lint/result/ArcanistLintResult.php create mode 100644 src/lint/result/__init__.php create mode 100644 src/lint/severity/ArcanistLintSeverity.php create mode 100644 src/lint/severity/__init__.php create mode 100644 src/parser/bundle/ArcanistBundle.php create mode 100644 src/parser/bundle/__init__.php create mode 100644 src/parser/diff/ArcanistDiffParser.php create mode 100644 src/parser/diff/__init__.php create mode 100644 src/parser/diff/__tests__/ArcanistDiffParserTestCase.php create mode 100644 src/parser/diff/__tests__/__init__.php create mode 100644 src/parser/diff/__tests__/data/basic-binary.udiff create mode 100755 src/parser/diff/__tests__/data/basic-missing-both-newlines-plus.udiff create mode 100644 src/parser/diff/__tests__/data/basic-missing-both-newlines.udiff create mode 100755 src/parser/diff/__tests__/data/basic-missing-new-newline-plus.udiff create mode 100644 src/parser/diff/__tests__/data/basic-missing-new-newline.udiff create mode 100755 src/parser/diff/__tests__/data/basic-missing-old-newline-plus.udiff create mode 100644 src/parser/diff/__tests__/data/basic-missing-old-newline.udiff create mode 100644 src/parser/diff/__tests__/data/basic-multi-hunk-content.svndiff create mode 100644 src/parser/diff/__tests__/data/basic-multi-hunk.udiff create mode 100644 src/parser/diff/__tests__/data/git-binary-change.gitdiff create mode 100755 src/parser/diff/__tests__/data/git-commit.gitdiff create mode 100755 src/parser/diff/__tests__/data/git-copy-plus.gitdiff create mode 100755 src/parser/diff/__tests__/data/git-copy.gitdiff create mode 100755 src/parser/diff/__tests__/data/git-delete-file.gitdiff create mode 100644 src/parser/diff/__tests__/data/git-filemode-change-only.gitdiff create mode 100644 src/parser/diff/__tests__/data/git-filemode-change.gitdiff create mode 100755 src/parser/diff/__tests__/data/git-ignore-whitespace-only.gitdiff create mode 100755 src/parser/diff/__tests__/data/git-merge-header.gitdiff create mode 100755 src/parser/diff/__tests__/data/git-move-edit.gitdiff create mode 100755 src/parser/diff/__tests__/data/git-move-plus.gitdiff create mode 100755 src/parser/diff/__tests__/data/git-move.gitdiff create mode 100644 src/parser/diff/__tests__/data/git-new-file.gitdiff create mode 100644 src/parser/diff/__tests__/data/svn-binary-add.svndiff create mode 100755 src/parser/diff/__tests__/data/svn-binary-diff.svndiff create mode 100644 src/parser/diff/__tests__/data/svn-empty-file.svndiff create mode 100644 src/parser/diff/__tests__/data/svn-ignore-whitespace-only.svndiff create mode 100644 src/parser/diff/__tests__/data/svn-property-add.svndiff create mode 100755 src/parser/diff/__tests__/data/svn-property-delete.svndiff create mode 100755 src/parser/diff/__tests__/data/svn-property-merge.svndiff create mode 100644 src/parser/diff/__tests__/data/svn-property-merged.svndiff create mode 100755 src/parser/diff/__tests__/data/svn-property-modify.svndiff create mode 100644 src/parser/diff/__tests__/data/svn-property-multiline.svndiff create mode 100644 src/parser/diff/change/ArcanistDiffChange.php create mode 100644 src/parser/diff/change/__init__.php create mode 100644 src/parser/diff/changetype/ArcanistDiffChangeType.php create mode 100644 src/parser/diff/changetype/__init__.php create mode 100644 src/parser/diff/hunk/ArcanistDiffHunk.php create mode 100644 src/parser/diff/hunk/__init__.php create mode 100644 src/repository/api/base/ArcanistRepositoryAPI.php create mode 100644 src/repository/api/base/__init__.php create mode 100644 src/repository/api/git/ArcanistGitAPI.php create mode 100644 src/repository/api/git/__init__.php create mode 100644 src/repository/api/subversion/ArcanistSubversionAPI.php create mode 100644 src/repository/api/subversion/__init__.php create mode 100644 src/staticanalysis/parsers/phutilmodule/PhutilModuleRequirements.php create mode 100644 src/staticanalysis/parsers/phutilmodule/__init__.php create mode 100644 src/staticanalysis/parsers/xhpast/api/exception/XHPASTSyntaxErrorException.php create mode 100644 src/staticanalysis/parsers/xhpast/api/exception/__init__.php create mode 100644 src/staticanalysis/parsers/xhpast/api/list/XHPASTNodeList.php create mode 100644 src/staticanalysis/parsers/xhpast/api/list/__init__.php create mode 100644 src/staticanalysis/parsers/xhpast/api/node/XHPASTNode.php create mode 100644 src/staticanalysis/parsers/xhpast/api/node/__init__.php create mode 100644 src/staticanalysis/parsers/xhpast/api/token/XHPASTToken.php create mode 100644 src/staticanalysis/parsers/xhpast/api/token/__init__.php create mode 100644 src/staticanalysis/parsers/xhpast/api/tree/XHPASTTree.php create mode 100644 src/staticanalysis/parsers/xhpast/api/tree/__init__.php create mode 100644 src/staticanalysis/parsers/xhpast/bin/__init__.php create mode 100644 src/staticanalysis/parsers/xhpast/bin/xhpast_parse.php create mode 100644 src/staticanalysis/parsers/xhpast/constants/__init__.php create mode 100644 src/staticanalysis/parsers/xhpast/constants/parser_nodes.php create mode 100644 src/staticanalysis/parsers/xhpast/constants/parser_tokens.php create mode 100644 src/unit/engine/base/ArcanistBaseUnitTestEngine.php create mode 100644 src/unit/engine/base/__init__.php create mode 100644 src/unit/engine/phutil/PhutilUnitTestEngine.php create mode 100644 src/unit/engine/phutil/__init__.php create mode 100644 src/unit/engine/phutil/testcase/ArcanistPhutilTestCase.php create mode 100644 src/unit/engine/phutil/testcase/__init__.php create mode 100644 src/unit/engine/phutil/testcase/exception/ArcanistPhutilTestTerminatedException.php create mode 100644 src/unit/engine/phutil/testcase/exception/__init__.php create mode 100644 src/unit/result/ArcanistUnitTestResult.php create mode 100644 src/unit/result/__init__.php create mode 100644 src/workflow/amend/ArcanistAmendWorkflow.php create mode 100644 src/workflow/amend/__init__.php create mode 100644 src/workflow/base/ArcanistBaseWorkflow.php create mode 100644 src/workflow/base/__init__.php create mode 100644 src/workflow/commit/ArcanistCommitWorkflow.php create mode 100644 src/workflow/commit/__init__.php create mode 100644 src/workflow/cover/ArcanistCoverWorkflow.php create mode 100644 src/workflow/cover/__init__.php create mode 100644 src/workflow/diff/ArcanistDiffWorkflow.php create mode 100644 src/workflow/diff/__init__.php create mode 100644 src/workflow/export/ArcanistExportWorkflow.php create mode 100644 src/workflow/export/__init__.php create mode 100644 src/workflow/git-hook-pre-receive/ArcanistGitHookPreReceiveWorkflow.php create mode 100644 src/workflow/git-hook-pre-receive/__init__.php create mode 100644 src/workflow/help/ArcanistHelpWorkflow.php create mode 100644 src/workflow/help/__init__.php create mode 100644 src/workflow/lint/ArcanistLintWorkflow.php create mode 100644 src/workflow/lint/__init__.php create mode 100644 src/workflow/list/ArcanistListWorkflow.php create mode 100644 src/workflow/list/__init__.php create mode 100644 src/workflow/mark-committed/ArcanistMarkCommittedWorkflow.php create mode 100644 src/workflow/mark-committed/__init__.php create mode 100644 src/workflow/patch/ArcanistPatchWorkflow.php create mode 100644 src/workflow/patch/__init__.php create mode 100644 src/workflow/unit/ArcanistUnitWorkflow.php create mode 100644 src/workflow/unit/__init__.php create mode 100644 src/workingcopyidentity/ArcanistWorkingCopyIdentity.php create mode 100644 src/workingcopyidentity/__init__.php create mode 100755 support/xhpast/Makefile create mode 100755 support/xhpast/ast.hpp create mode 100755 support/xhpast/astnode.cpp create mode 100755 support/xhpast/astnode.hpp create mode 100755 support/xhpast/generate_nodes.php create mode 100755 support/xhpast/node_names.hpp create mode 100755 support/xhpast/parser.y create mode 100755 support/xhpast/parser_nodes.php create mode 100755 support/xhpast/scanner.l create mode 100755 support/xhpast/xhpast.cpp diff --git a/.arcconfig b/.arcconfig new file mode 100644 index 00000000..0e88a403 --- /dev/null +++ b/.arcconfig @@ -0,0 +1,7 @@ +{ + "project_id" : "arcanist", + "conduit_uri" : "http://tools.epriestley-conduit.dev1557.facebook.com/api/", + "lint_engine" : "PhutilLintEngine", + "unit_engine" : "PhutilUnitTestEngine", + "copyright_holder" : "Facebook, Inc." +} diff --git a/.divinerconfig b/.divinerconfig new file mode 100644 index 00000000..311847da --- /dev/null +++ b/.divinerconfig @@ -0,0 +1,2 @@ +{} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f2d389f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.a +*.o +/support/xhpast/xhpast +/src/staticanalysis/parsers/xhpast/bin/xhpast +parser.yacc.cpp +parser.yacc.hpp +scanner.lex.cpp +scanner.lex.hpp +parser.yacc.output diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..60686e6c --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2011 Facebook, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README b/README new file mode 100644 index 00000000..a401ac21 --- /dev/null +++ b/README @@ -0,0 +1,11 @@ +PROJECT STATUS: CAVEAT EMPTOR + +This is an unstable preview release. I'm open sourcing some of Facebook's +internal tools, but they'll be unstable for at least a couple months. +-epriestley + + +WHAT IS ARCANIST? + +Arcanist is the CLI for Facebook's code review tool, Differential. Since +Differential isn't released yet, it may not be terribly useful on its own. diff --git a/bin/arc b/bin/arc new file mode 120000 index 00000000..7aac748c --- /dev/null +++ b/bin/arc @@ -0,0 +1 @@ +../scripts/arcanist.php \ No newline at end of file diff --git a/externals/README b/externals/README new file mode 100644 index 00000000..8e66b78a --- /dev/null +++ b/externals/README @@ -0,0 +1,2 @@ +This directory contains third party open source software which is bundled with +Arcanist. diff --git a/externals/pep8/README b/externals/pep8/README new file mode 100644 index 00000000..d719aa49 --- /dev/null +++ b/externals/pep8/README @@ -0,0 +1,3 @@ +pep8.py was written by Johann Rocholl. The main page for the project is here: + + https://github.com/jcrocholl/pep8 diff --git a/scripts/__init_script__.php b/scripts/__init_script__.php new file mode 100644 index 00000000..a69ac9a5 --- /dev/null +++ b/scripts/__init_script__.php @@ -0,0 +1,32 @@ + $arg) { + if ($arg == '--') { + break; + } else if ($arg == '--trace') { + unset($args[$key]); + $config_trace_mode = true; + } +} + +$args = array_values($args); + +try { + + if ($config_trace_mode) { + ExecFuture::pushEchoMode(true); + } + + if (!$args) { + throw new ArcanistUsageException("No command provided. Try 'arc help'."); + } + + $working_copy = ArcanistWorkingCopyIdentity::newFromPath($_SERVER['PWD']); + $libs = $working_copy->getConfig('phutil_libraries'); + if ($libs) { + foreach ($libs as $name => $location) { + if ($config_trace_mode) { + echo "Loading phutil library '{$name}' from '{$location}'...\n"; + } + $library_root = Filesystem::resolvePath( + $location, + $working_copy->getProjectRoot()); + phutil_load_library($library_root); + } + } + + $config = $working_copy->getConfig('arcanist_configuration'); + if ($config) { + phutil_autoload_class($config); + $config = new $config(); + } else { + $config = new ArcanistConfiguration(); + } + + $command = strtolower($args[0]); + $workflow = $config->buildWorkflow($command); + if (!$workflow) { + throw new ArcanistUsageException( + "Unknown command '{$command}'. Try 'arc help'."); + } + $workflow->setArcanistConfiguration($config); + $workflow->setCommand($command); + $workflow->parseArguments(array_slice($args, 1)); + + $need_working_copy = $workflow->requiresWorkingCopy(); + $need_conduit = $workflow->requiresConduit(); + $need_auth = $workflow->requiresAuthentication(); + $need_repository_api = $workflow->requiresRepositoryAPI(); + + $need_conduit = $need_conduit || + $need_auth; + $need_working_copy = $need_working_copy || + $need_conduit || + $need_repository_api; + + if ($need_working_copy) { + $workflow->setWorkingCopy($working_copy); + } + + if ($need_conduit) { + $conduit_uri = $working_copy->getConduitURI(); + if (!$conduit_uri) { + throw new ArcanistUsageException( + "No Conduit URI is specified in the .arcconfig file for this project. ". + "Specify the Conduit URI for the host Differential is running on."); + } + $conduit = new ConduitClient($conduit_uri); + $conduit->setTraceMode($config_trace_mode); + $workflow->setConduit($conduit); + + $description = implode(' ', $argv); + $connection = $conduit->callMethodSynchronous( + 'conduit.connect', + array( + 'client' => 'arc', + 'clientVersion' => 1, + 'clientDescription' => php_uname('n').':'.$description, + 'user' => getenv('USER'), + )); + $conduit->setConnectionID($connection['connectionID']); + } + + if ($need_repository_api) { + $repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity( + $working_copy); + $workflow->setRepositoryAPI($repository_api); + } + + if ($need_auth) { + $user_name = getenv('USER'); + $user_find_future = $conduit->callMethod( + 'user.find', + array( + 'aliases' => array( + $user_name, + ), + )); + $user_guids = $user_find_future->resolve(); + if (empty($user_guids[$user_name])) { + throw new ArcanistUsageException( + "Username '{$user_name}' is not recognized."); + } + + $user_guid = $user_guids[$user_name]; + $workflow->setUserGUID($user_guid); + $workflow->setUserName($user_name); + } + + $config->willRunWorkflow($command, $workflow); + $workflow->willRunWorkflow(); + $err = $workflow->run(); + if ($err == 0) { + $config->didRunWorkflow($command, $workflow); + } + exit($err); + +} catch (ArcanistUsageException $ex) { + echo phutil_console_format( + "**Usage Exception:** %s\n", + $ex->getMessage()); + if ($config_trace_mode) { + echo "\n"; + throw $ex; + } + + exit(1); +} catch (Exception $ex) { + if ($config_trace_mode) { + throw $ex; + } + + echo phutil_console_format( + "\n**Exception:**\n%s\n%s\n", + $ex->getMessage(), + "(Run with --trace for a full exception trace.)"); + + exit(1); +} diff --git a/scripts/phutil_analyzer.php b/scripts/phutil_analyzer.php new file mode 100755 index 00000000..0e69a274 --- /dev/null +++ b/scripts/phutil_analyzer.php @@ -0,0 +1,364 @@ +#!/usr/bin/env php + array_fill_keys($builtin_classes, true), + 'function' => array_fill_keys($builtin_functions, true) + array( + 'empty' => true, + 'isset' => true, + 'echo' => true, + 'print' => true, + 'exit' => true, + 'die' => true, + + 'phutil_module_exists' => true, + ), + 'interface' => array_fill_keys($builtin_interfaces, true), +); + +require_once dirname(__FILE__).'/__init_script__.php'; + +if ($argc != 2) { + $self = basename($argv[0]); + echo "usage: {$self} \n"; + exit(1); +} + +phutil_require_module('phutil', 'filesystem'); +$dir = Filesystem::resolvePath($argv[1]); + +phutil_require_module('arcanist', 'staticanalysis/parsers/xhpast/bin'); +phutil_require_module('arcanist', 'lint/linter/phutilmodule'); +phutil_require_module('arcanist', 'lint/message'); + +$data = array(); +$futures = array(); +foreach (Filesystem::listDirectory($dir, $hidden_files = false) as $file) { + if (!preg_match('/.php$/', $file)) { + continue; + } + $data[$file] = Filesystem::readFile($dir.'/'.$file); + $futures[$file] = xhpast_get_parser_future($data[$file]); +} + +phutil_require_module('arcanist', 'staticanalysis/parsers/xhpast/api/tree'); +phutil_require_module('arcanist', 'staticanalysis/parsers/phutilmodule'); + +$requirements = new PhutilModuleRequirements(); +$requirements->addBuiltins($builtin); + +$has_init = false; +$has_files = false; +foreach (Futures($futures) as $file => $future) { + + try { + $tree = XHPASTTree::newFromDataAndResolvedExecFuture( + $data[$file], + $future->resolve()); + } catch (XHPASTSyntaxErrorException $ex) { + echo "Syntax Error! In '{$file}': ".$ex->getMessage()."\n"; + exit(1); + } + + $root = $tree->getRootNode(); + $requirements->setCurrentFile($file); + + if ($file == '__init__.php') { + $has_init = true; + $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); + foreach ($calls as $call) { + $name = $call->getChildByIndex(0); + $call_name = $name->getConcreteString(); + if ($call_name == 'phutil_require_source') { + $params = $call->getChildByIndex(1)->getChildren(); + if (count($params) !== 1) { + $requirements->addLint( + $call, + $call->getConcreteString(), + ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, + "Call to phutil_require_source() must have exactly one argument."); + continue; + } + $param = reset($params); + $value = $param->getStringLiteralValue(); + if ($value === null) { + $requirements->addLint( + $param, + $param->getConcreteString(), + ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, + "phutil_require_source() parameter must be a string literal."); + continue; + } + $requirements->addSourceDependency($name, $value); + } else if ($call_name == 'phutil_require_module') { + analyze_require_module($call, $requirements); + } + } + } else { + $has_files = true; + + $requirements->addSourceDeclaration(basename($file)); + + // Function uses: + // - Explicit call + // TODO?: String literal in ReflectionFunction(). + + $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); + foreach ($calls as $call) { + $name = $call->getChildByIndex(0); + if ($name->getTypeName() == 'n_VARIABLE' || + $name->getTypeName() == 'n_VARIABLE_VARIABLE') { + $requirements->addLint( + $name, + $name->getConcreteString(), + ArcanistPhutilModuleLinter::LINT_ANALYZER_DYNAMIC, + "Use of variable function calls prevents dependencies from being ". + "checked statically. This module may have undetectable errors."); + continue; + } + if ($name->getTypeName() == 'n_CLASS_STATIC_ACCESS') { + // We'll pick this up later. + continue; + } + + $call_name = $name->getConcreteString(); + if ($call_name == 'phutil_require_module') { + analyze_require_module($call, $requirements); + } else if ($call_name == 'call_user_func' || + $call_name == 'call_user_func_array') { + $params = $call->getChildByIndex(1)->getChildren(); + if (count($params) == 0) { + $requirements->addLint( + $call, + $call->getConcreteString(), + ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, + "Call to {$call_name}() must have at least one argument."); + } + $symbol = array_shift($params); + $symbol_value = $symbol->getStringLiteralValue(); + if ($symbol_value) { + $requirements->addFunctionDependency( + $symbol, + $symbol_value); + } else { + $requirements->addLint( + $symbol, + $symbol->getConcreteString(), + ArcanistPhutilModuleLinter::LINT_ANALYZER_DYNAMIC, + "Use of variable arguments to {$call_name} prevents dependencies ". + "from being checked statically. This module may have undetectable ". + "errors."); + } + } else { + $requirements->addFunctionDependency( + $name, + $name->getConcreteString()); + } + } + + $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); + foreach ($functions as $function) { + $name = $function->getChildByIndex(2); + $requirements->addFunctionDeclaration( + $name, + $name->getConcreteString()); + } + + + // Class uses: + // - new + // - extends (in class declaration) + // - Static method call + // - Static property access + // - Constant use + // TODO?: String literal in ReflectionClass(). + // TODO?: String literal in array literal in call_user_func / + // call_user_func_array(). + + $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); + foreach ($classes as $class) { + $class_name = $class->getChildByIndex(1); + $requirements->addClassDeclaration( + $class_name, + $class_name->getConcreteString()); + $extends = $class->getChildByIndex(2); + foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { + $requirements->addClassDependency( + $class_name->getConcreteString(), + $parent, + $parent->getConcreteString()); + } + $implements = $class->getChildByIndex(3); + $interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME'); + foreach ($interfaces as $interface) { + $requirements->addInterfaceDependency( + $class_name->getConcreteString(), + $interface, + $interface->getConcreteString()); + } + } + + if (count($classes) > 1) { + foreach ($classes as $class) { + $class_name = $class->getChildByIndex(1); + $class_string = $class_name->getConcreteString(); + $requirements->addLint( + $class_name, + $class_string, + ArcanistPhutilModuleLinter::LINT_ANALYZER_MULTIPLE_CLASSES, + "This file declares more than one class. Declare only one class per ". + "file."); + break; + } + } else if (count($classes) == 1) { + foreach ($classes as $class) { + $class_name = $class->getChildByIndex(1); + $class_string = $class_name->getConcreteString(); + if ($file != $class_string.'.php') { + $rename = $class_string.'.php'; + $requirements->addLint( + $class_name, + $class_string, + ArcanistPhutilModuleLinter::LINT_ANALYZER_CLASS_FILENAME, + "The name of this file differs from the name of the class it ". + "declares. Rename the file to '{$rename}'."); + } + break; + } + } + + $uses_of_new = $root->selectDescendantsOfType('n_NEW'); + foreach ($uses_of_new as $new_operator) { + $name = $new_operator->getChildByIndex(0); + if ($name->getTypeName() == 'n_VARIABLE' || + $name->getTypeName() == 'n_VARIABLE_VARIABLE') { + $requirements->addLint( + $name, + $name->getConcreteString(), + ArcanistPhutilModuleLinter::LINT_ANALYZER_DYNAMIC, + "Use of variable class instantiation prevents dependencies from ". + "being checked statically. This module may have undetectable ". + "errors."); + continue; + } + $requirements->addClassDependency( + null, + $name, + $name->getConcreteString()); + } + + $static_uses = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); + foreach ($static_uses as $static_use) { + $name = $static_use->getChildByIndex(0); + if ($name->getTypeName() != 'n_CLASS_NAME') { + echo "WARNING UNLINTABLE\n"; + continue; + } + $name_concrete = $name->getConcreteString(); + $magic_names = array( + 'static' => true, + 'parent' => true, + 'self' => true, + ); + if (isset($magic_names[$name_concrete])) { + continue; + } + $requirements->addClassDependency( + null, + $name, + $name_concrete); + } + + // Interface uses: + // - implements + // - extends (in interface declaration) + + $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); + foreach ($interfaces as $interface) { + $interface_name = $interface->getChildByIndex(1); + $requirements->addInterfaceDeclaration( + $interface_name, + $interface_name->getConcreteString()); + $extends = $interface->getChildByIndex(2); + foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { + $requirements->addInterfaceDependency( + $class_name->getConcreteString(), + $parent, + $parent->getConcreteString()); + } + } + + } +} + +if (!$has_init && $has_files) { + $requirements->addRawLint( + ArcanistPhutilModuleLinter::LINT_ANALYZER_NO_INIT, + "Create an __init__.php file in this module."); +} + +echo json_encode($requirements->toDictionary()); + +function analyze_require_module( + XHPASTNode $call, + PhutilModuleRequirements $requirements) { + + $name = $call->getChildByIndex(0); + $params = $call->getChildByIndex(1)->getChildren(); + if (count($params) !== 2) { + $requirements->addLint( + $call, + $call->getConcreteString(), + ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, + "Call to phutil_require_module() must have exactly two arguments."); + return; + } + + $module_param = array_pop($params); + $library_param = array_pop($params); + + $library_value = $library_param->getStringLiteralValue(); + if ($library_value === null) { + $requirements->addLint( + $library_param, + $library_param->getConcreteString(), + ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, + "phutil_require_module() parameters must be string literals."); + return; + } + + $module_value = $module_param->getStringLiteralValue(); + if ($module_value === null) { + $requirements->addLint( + $module_param, + $module_param->getConcreteString(), + ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE, + "phutil_require_module() parameters must be string literals."); + return; + } + + $requirements->addModuleDependency( + $name, + $library_value.':'.$module_value); +} diff --git a/scripts/phutil_mapper.php b/scripts/phutil_mapper.php new file mode 100755 index 00000000..20d9235e --- /dev/null +++ b/scripts/phutil_mapper.php @@ -0,0 +1,128 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +phutil_require_module('phutil', 'filesystem'); +phutil_require_module('phutil', 'future/exec'); + +$root = Filesystem::resolvePath($argv[1]); + +if (!@file_exists($root.'/__phutil_library_init__.php')) { + throw new Exception("Provided path is not a phutil library."); +} + +echo "Finding phutil modules...\n"; + +list($stdout) = execx( + "(cd %s && find . -type d -path '*/.*' -prune -o -type d -print0)", + $root); + +$futures = array(); +foreach (array_filter(explode("\0", $stdout)) as $dir) { + if ($dir == '.') { + continue; + } + $module = preg_replace('@^\\./@', '', $dir); + $futures[$module] = new ExecFuture( + '%s %s', + dirname(__FILE__).'/phutil_analyzer.php', + $root.'/'.$module); +} + +echo "Analyzing ".number_format(count($futures))." modules"; +$class_map = array(); +$requires_class_map = array(); +$requires_interface_map = array(); +$function_map = array(); +foreach (Futures($futures)->limit(16) as $module => $future) { + echo "."; + $spec = $future->resolveJSON(); + foreach (array('class', 'interface') as $type) { + foreach ($spec['declares'][$type] as $class => $where) { + if (!empty($class_map[$class])) { + $prior = $class_map[$class]; + echo "\n"; + echo "Error: definition of {$type} '{$class}' in module '{$module}' ". + "duplicates prior definition in module '{$prior}'."; + echo "\n"; + exit(1); + } + $class_map[$class] = $module; + } + } + if (!empty($spec['chain']['class'])) { + $requires_class_map += $spec['chain']['class']; + } + if (!empty($spec['chain']['interface'])) { + $requires_interface_map += $spec['chain']['interface']; + } + foreach ($spec['declares']['function'] as $function => $where) { + if (!empty($function_map[$function])) { + $prior = $function_map[$function]; + echo "\n"; + echo "Error: definition of function '{$function}' in module '{$module}' ". + "duplicates prior definition in module '{$prior}'."; + echo "\n"; + exit(1); + } + $function_map[$function] = $module; + } +} +echo "\n"; + +ksort($class_map); +ksort($requires_class_map); +ksort($requires_interface_map); +ksort($function_map); + +$library_map = array( + 'class' => $class_map, + 'function' => $function_map, + 'requires_class' => $requires_class_map, + 'requires_interface' => $requires_interface_map, +); +$library_map = var_export($library_map, $return_string = true); +$library_map = preg_replace('/\s+$/m', '', $library_map); +$library_map = preg_replace('/array \(/', 'array(', $library_map); + +$at = '@'; +$map_file = << + array( + 'ArcanistAmendWorkflow' => 'workflow/amend', + 'ArcanistApacheLicenseLinter' => 'lint/linter/apachelicense', + 'ArcanistBaseUnitTestEngine' => 'unit/engine/base', + 'ArcanistBaseWorkflow' => 'workflow/base', + 'ArcanistBundle' => 'parser/bundle', + 'ArcanistChooseInvalidRevisionException' => 'exception', + 'ArcanistChooseNoRevisionsException' => 'exception', + 'ArcanistCommitWorkflow' => 'workflow/commit', + 'ArcanistConfiguration' => 'configuration', + 'ArcanistCoverWorkflow' => 'workflow/cover', + 'ArcanistDiffChange' => 'parser/diff/change', + 'ArcanistDiffChangeType' => 'parser/diff/changetype', + 'ArcanistDiffHunk' => 'parser/diff/hunk', + 'ArcanistDiffParser' => 'parser/diff', + 'ArcanistDiffParserTestCase' => 'parser/diff/__tests__', + 'ArcanistDiffWorkflow' => 'workflow/diff', + 'ArcanistDifferentialCommitMessage' => 'differential/commitmessage', + 'ArcanistDifferentialCommitMessageParserException' => 'differential/commitmessage', + 'ArcanistDifferentialRevisionRef' => 'differential/revision', + 'ArcanistExportWorkflow' => 'workflow/export', + 'ArcanistFilenameLinter' => 'lint/linter/filename', + 'ArcanistGeneratedLinter' => 'lint/linter/generated', + 'ArcanistGitAPI' => 'repository/api/git', + 'ArcanistGitHookPreReceiveWorkflow' => 'workflow/git-hook-pre-receive', + 'ArcanistHelpWorkflow' => 'workflow/help', + 'ArcanistLintEngine' => 'lint/engine/base', + 'ArcanistLintMessage' => 'lint/message', + 'ArcanistLintPatcher' => 'lint/patcher', + 'ArcanistLintRenderer' => 'lint/renderer', + 'ArcanistLintResult' => 'lint/result', + 'ArcanistLintSeverity' => 'lint/severity', + 'ArcanistLintWorkflow' => 'workflow/lint', + 'ArcanistLinter' => 'lint/linter/base', + 'ArcanistListWorkflow' => 'workflow/list', + 'ArcanistMarkCommittedWorkflow' => 'workflow/mark-committed', + 'ArcanistNoEffectException' => 'exception/usage/noeffect', + 'ArcanistNoEngineException' => 'exception/usage/noengine', + 'ArcanistPEP8Linter' => 'lint/linter/pep8', + 'ArcanistPatchWorkflow' => 'workflow/patch', + 'ArcanistPhutilModuleLinter' => 'lint/linter/phutilmodule', + 'ArcanistPhutilTestCase' => 'unit/engine/phutil/testcase', + 'ArcanistPhutilTestTerminatedException' => 'unit/engine/phutil/testcase/exception', + 'ArcanistRepositoryAPI' => 'repository/api/base', + 'ArcanistSubversionAPI' => 'repository/api/subversion', + 'ArcanistTextLinter' => 'lint/linter/text', + 'ArcanistUnitTestResult' => 'unit/result', + 'ArcanistUnitWorkflow' => 'workflow/unit', + 'ArcanistUsageException' => 'exception/usage', + 'ArcanistUserAbortException' => 'exception/usage/userabort', + 'ArcanistWorkingCopyIdentity' => 'workingcopyidentity', + 'ArcanistXHPASTLinter' => 'lint/linter/xhpast', + 'ArcanistXHPASTLinterTestCase' => 'lint/linter/xhpast/__tests__', + 'PhutilLintEngine' => 'lint/engine/phutil', + 'PhutilModuleRequirements' => 'staticanalysis/parsers/phutilmodule', + 'PhutilUnitTestEngine' => 'unit/engine/phutil', + 'UnitTestableArcanistLintEngine' => 'lint/engine/test', + 'XHPASTNode' => 'staticanalysis/parsers/xhpast/api/node', + 'XHPASTNodeList' => 'staticanalysis/parsers/xhpast/api/list', + 'XHPASTSyntaxErrorException' => 'staticanalysis/parsers/xhpast/api/exception', + 'XHPASTToken' => 'staticanalysis/parsers/xhpast/api/token', + 'XHPASTTree' => 'staticanalysis/parsers/xhpast/api/tree', + ), + 'function' => + array( + 'xhp_parser_node_constants' => 'staticanalysis/parsers/xhpast/constants', + 'xhpast_get_parser_future' => 'staticanalysis/parsers/xhpast/bin', + 'xhpast_parser_token_constants' => 'staticanalysis/parsers/xhpast/constants', + ), + 'requires_class' => + array( + 'ArcanistAmendWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistApacheLicenseLinter' => 'ArcanistLinter', + 'ArcanistCommitWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistDiffParserTestCase' => 'ArcanistPhutilTestCase', + 'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistFilenameLinter' => 'ArcanistLinter', + 'ArcanistGeneratedLinter' => 'ArcanistLinter', + 'ArcanistGitAPI' => 'ArcanistRepositoryAPI', + 'ArcanistGitHookPreReceiveWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistHelpWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistLintWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistListWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistNoEffectException' => 'ArcanistUsageException', + 'ArcanistNoEngineException' => 'ArcanistUsageException', + 'ArcanistPEP8Linter' => 'ArcanistLinter', + 'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistPhutilModuleLinter' => 'ArcanistLinter', + 'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI', + 'ArcanistTextLinter' => 'ArcanistLinter', + 'ArcanistUnitWorkflow' => 'ArcanistBaseWorkflow', + 'ArcanistUserAbortException' => 'ArcanistUsageException', + 'ArcanistXHPASTLinter' => 'ArcanistLinter', + 'ArcanistXHPASTLinterTestCase' => 'ArcanistPhutilTestCase', + 'PhutilLintEngine' => 'ArcanistLintEngine', + 'PhutilUnitTestEngine' => 'ArcanistBaseUnitTestEngine', + 'UnitTestableArcanistLintEngine' => 'ArcanistLintEngine', + ), + 'requires_interface' => + array( + ), +)); diff --git a/src/configuration/ArcanistConfiguration.php b/src/configuration/ArcanistConfiguration.php new file mode 100644 index 00000000..17a4d4fe --- /dev/null +++ b/src/configuration/ArcanistConfiguration.php @@ -0,0 +1,80 @@ +rawCorpus = $corpus; + + $match = null; + if (preg_match('/^Differential Revision:\s*D?(\d+)/m', $corpus, $match)) { + $obj->revisionID = (int)$match[1]; + } + + $pattern = '/^git-svn-id:\s*([^@]+)@(\d+)\s+(.*)$/m'; + if (preg_match($pattern, $corpus, $match)) { + $obj->gitSVNBaseRevision = $match[1].'@'.$match[2]; + $obj->gitSVNBasePath = $match[1]; + $obj->gitSVNUUID = $match[3]; + } + + return $obj; + } + + public function getRawCorpus() { + return $this->rawCorpus; + } + + public function getRevisionID() { + return $this->revisionID; + } + + public function pullDataFromConduit(ConduitClient $conduit) { + $result = $conduit->callMethod( + 'differential.parsecommitmessage', + array( + 'corpus' => $this->rawCorpus, + )); + $result = $result->resolve(); + if (!empty($result['error'])) { + throw new ArcanistDifferentialCommitMessageParserException( + $result['error']); + } + $this->fields = $result['fields']; + } + + public function getFieldValue($key) { + if (array_key_exists($key, $this->fields)) { + return $this->fields[$key]; + } + return null; + } + + public function getFields() { + return $this->fields; + } + + public function getGitSVNBaseRevision() { + return $this->gitSVNBaseRevision; + } + + public function getGitSVNBasePath() { + return $this->gitSVNBasePath; + } + + public function getGitSVNUUID() { + return $this->gitSVNUUID; + } + +} diff --git a/src/differential/commitmessage/ArcanistDifferentialCommitMessageParserException.php b/src/differential/commitmessage/ArcanistDifferentialCommitMessageParserException.php new file mode 100644 index 00000000..37122995 --- /dev/null +++ b/src/differential/commitmessage/ArcanistDifferentialCommitMessageParserException.php @@ -0,0 +1,21 @@ +id = $dictionary['id']; + $ref->name = $dictionary['name']; + $ref->statusName = $dictionary['statusName']; + $ref->sourcePath = $dictionary['sourcePath']; + return $ref; + } + + protected function __construct() { + + } + + public function getID() { + return $this->id; + } + + public function getName() { + return $this->name; + } + + public function getStatusName() { + return $this->statusName; + } + + public function getSourcePath() { + return $this->sourcePath; + } + +} diff --git a/src/differential/revision/__init__.php b/src/differential/revision/__init__.php new file mode 100644 index 00000000..1d454f9a --- /dev/null +++ b/src/differential/revision/__init__.php @@ -0,0 +1,19 @@ +workingCopy = $working_copy; + return $this; + } + + public function getWorkingCopy() { + return $this->workingCopy; + } + + public function setPaths($paths) { + $this->paths = $paths; + return $this; + } + + public function getPaths() { + return $this->paths; + } + + public function setPathChangedLines($path, array $changed) { + $this->changedLines[$path] = array_fill_keys($changed, true); + return $this; + } + + public function getPathChangedLines($path) { + return idx($this->changedLines, $path); + } + + protected function loadData($path) { + if (!isset($this->fileData[$path])) { + $disk_path = $this->getFilePathOnDisk($path); + $this->fileData[$path] = Filesystem::readFile($disk_path); + } + return $this->fileData[$path]; + } + + public function getFilePathOnDisk($path) { + return $path; + } + + public function setMinimumSeverity($severity) { + $this->minimumSeverity = $severity; + return $this; + } + + public function getCommitHookMode() { + return false; + } + + public function run() { + $stopped = array(); + $linters = $this->buildLinters(); + + if (!$linters) { + throw new ArcanistNoEffectException("No linters to run."); + } + + $have_paths = false; + foreach ($linters as $linter) { + if ($linter->getPaths()) { + $have_paths = true; + break; + } + } + + if (!$have_paths) { + throw new ArcanistNoEffectException("No paths are lintable."); + } + + foreach ($linters as $linter) { + $linter->setEngine($this); + $paths = $linter->getPaths(); + + foreach ($paths as $key => $path) { + // Make sure each path has a result generated, even if it is empty + // (i.e., the file has no lint messages). + $result = $this->getResultForPath($path); + if (isset($stopped[$path])) { + unset($paths[$key]); + } + } + $paths = array_values($paths); + + if ($paths) { + $linter->willLintPaths($paths); + foreach ($paths as $path) { + $linter->willLintPath($path); + $linter->lintPath($path); + if ($linter->didStopAllLinters()) { + $stopped[$path] = true; + } + } + } + + $minimum = $this->minimumSeverity; + foreach ($linter->getLintMessages() as $message) { + if (!ArcanistLintSeverity::isAtLeastAsSevere($message, $minimum)) { + continue; + } + // When a user runs "arc diff", we default to raising only warnings on + // lines they have changed (errors are still raised anywhere in the + // file). + $changed = $this->getPathChangedLines($path); + if ($changed !== null && !$message->isError()) { + if (empty($changed[$message->getLine()])) { + continue; + } + } + $result = $this->getResultForPath($message->getPath()); + $result->addMessage($message); + } + } + + foreach ($this->results as $path => $result) { + if (isset($this->fileData[$path])) { + // Only set the data if any linter loaded it. The goal here is to + // avoid binaries when we don't actually care about their contents, + // for performance. + $result->setData($this->fileData[$path]); + $result->setFilePathOnDisk($this->getFilePathOnDisk($path)); + } + } + + return $this->results; + } + + abstract protected function buildLinters(); + + private function getResultForPath($path) { + if (empty($this->results[$path])) { + $result = new ArcanistLintResult(); + $result->setPath($path); + $this->results[$path] = $result; + } + return $this->results[$path]; + } + + public function getLineAndCharFromOffset($path, $offset) { + if (!isset($this->charToLine[$path])) { + $char_to_line = array(); + $line_to_first_char = array(); + + $lines = explode("\n", $this->loadData($path)); + $line_number = 0; + $line_start = 0; + foreach ($lines as $line) { + $len = strlen($line) + 1; // Account for "\n". + $line_to_first_char[] = $line_start; + $line_start += $len; + for ($ii = 0; $ii < $len; $ii++) { + $char_to_line[] = $line_number; + } + $line_number++; + } + $this->charToLine[$path] = $char_to_line; + $this->lineToFirstChar[$path] = $line_to_first_char; + } + + $line = $this->charToLine[$path][$offset]; + $char = $offset - $this->lineToFirstChar[$path][$line]; + + return array($line, $char); + } + + +} diff --git a/src/lint/engine/base/__init__.php b/src/lint/engine/base/__init__.php new file mode 100644 index 00000000..28fe981f --- /dev/null +++ b/src/lint/engine/base/__init__.php @@ -0,0 +1,17 @@ +getPaths(); + + // This needs to go first so that changes to generated files cause module + // linting. This linter also operates on removed files, because removing + // a file changes the static properties of a module. + $module_linter = new ArcanistPhutilModuleLinter(); + $linters[] = $module_linter; + foreach ($paths as $path) { + $module_linter->addPath($path); + } + + // Remaining lint engines operate on file contents and ignore removed + // files. + foreach ($paths as $key => $path) { + if (!$this->pathExists($path)) { + unset($paths[$key]); + } + } + + $generated_linter = new ArcanistGeneratedLinter(); + $linters[] = $generated_linter; + + $text_linter = new ArcanistTextLinter(); + $linters[] = $text_linter; + foreach ($paths as $path) { + $is_text = false; + if (preg_match('/\.php$/', $path)) { + $is_text = true; + } + if ($is_text) { + $generated_linter->addPath($path); + $generated_linter->addData($path, $this->loadData($path)); + + $text_linter->addPath($path); + $text_linter->addData($path, $this->loadData($path)); + } + } + + $name_linter = new ArcanistFilenameLinter(); + $linters[] = $name_linter; + foreach ($paths as $path) { + $name_linter->addPath($path); + } + + $xhpast_linter = new ArcanistXHPASTLinter(); + $license_linter = new ArcanistApacheLicenseLinter(); + $linters[] = $xhpast_linter; + $linters[] = $license_linter; + foreach ($paths as $path) { + if (preg_match('/\.php$/', $path)) { + $xhpast_linter->addPath($path); + $xhpast_linter->addData($path, $this->loadData($path)); + } + } + + foreach ($paths as $path) { + if (preg_match('/\.(php|cpp|hpp|l|y)$/', $path)) { + if (!preg_match('@^externals/@', $path)) { + $license_linter->addPath($path); + $license_linter->addData($path, $this->loadData($path)); + } + } + } + + return $linters; + } + + public function pathExists($path) { + $disk_path = $this->getFilePathOnDisk($path); + return Filesystem::pathExists($disk_path); + } + +} diff --git a/src/lint/engine/phutil/__init__.php b/src/lint/engine/phutil/__init__.php new file mode 100644 index 00000000..c9a4bd11 --- /dev/null +++ b/src/lint/engine/phutil/__init__.php @@ -0,0 +1,20 @@ +linters[] = $linter; + return $this; + } + + public function addFileData($path, $data) { + $this->fileData[$path] = $data; + return $this; + } + + protected function buildLinters() { + return $this->linters; + } + +} diff --git a/src/lint/engine/test/__init__.php b/src/lint/engine/test/__init__.php new file mode 100644 index 00000000..b9e07ded --- /dev/null +++ b/src/lint/engine/test/__init__.php @@ -0,0 +1,12 @@ + 'No License Header', + ); + } + + public function lintPath($path) { + $working_copy = $this->getEngine()->getWorkingCopy(); + $copyright_holder = $working_copy->getConfig('copyright_holder'); + + if (!$copyright_holder) { + throw new ArcanistUsageException( + "This project uses the ArcanistApacheLicenseLinter, but does not ". + "define a 'copyright_holder' in its .arcconfig."); + } + + $year = date('Y'); + + $maybe_php_or_script = '(#![^\n]+?[\n])?(<[?]php\s+?)?'; + $patterns = array( + "@^{$maybe_php_or_script}//[^\n]*Copyright[^\n]*[\n]\s*@i", + "@^{$maybe_php_or_script}/[*].*?Copyright.*?[*]/\s*@is", + "@^{$maybe_php_or_script}\s*@", + ); + + $license = <<getData($path); + $matches = 0; + if (preg_match($pattern, $data, $matches)) { + $expect = rtrim(implode('', array_slice($matches, 1)))."\n\n".$license; + $expect = ltrim($expect); + if (rtrim($matches[0]) != rtrim($expect)) { + $this->raiseLintAtOffset( + 0, + self::LINT_NO_LICENSE_HEADER, + 'This file has a missing or out of date license header.', + $matches[0], + $expect); + } + break; + } + } + } + +} diff --git a/src/lint/linter/apachelicense/__init__.php b/src/lint/linter/apachelicense/__init__.php new file mode 100644 index 00000000..f16cf6da --- /dev/null +++ b/src/lint/linter/apachelicense/__init__.php @@ -0,0 +1,13 @@ +customSeverityMap = $map; + return $this; + } + + public function getActivePath() { + return $this->activePath; + } + + public function stopAllLinters() { + $this->stopAllLinters = true; + return $this; + } + + public function didStopAllLinters() { + return $this->stopAllLinters; + } + + public function addPath($path) { + $this->paths[$path] = $path; + return $this; + } + + public function getPaths() { + return array_values($this->paths); + } + + public function addData($path, $data) { + $this->data[$path] = $data; + return $this; + } + + protected function getData($path) { + if (!array_key_exists($path, $this->data)) { + throw new Exception("Data is not provided for path '{$path}'!"); + } + return $this->data[$path]; + } + + public function setEngine($engine) { + $this->engine = $engine; + return $this; + } + + protected function getEngine() { + return $this->engine; + } + + public function getLintMessageFullCode($short_code) { + return $this->getLinterName().$short_code; + } + + public function getLintMessageSeverity($code) { + $map = $this->customSeverityMap; + if (isset($map[$code])) { + return $map[$code]; + } + + $map = $this->getLintSeverityMap(); + if (isset($map[$code])) { + return $map[$code]; + } + + return ArcanistLintSeverity::SEVERITY_ERROR; + } + + public function getLintMessageName($code) { + $map = $this->getLintNameMap(); + if (isset($map[$code])) { + return $map[$code]; + } + return "Unknown lint message!"; + } + + protected function addLintMessage(ArcanistLintMessage $message) { + $this->messages[] = $message; + return $message; + } + + public function getLintMessages() { + return $this->messages; + } + + protected function raiseLintAtLine( + $line, + $char, + $code, + $desc, + $original = null, + $replacement = null) { + + $dict = array( + 'path' => $this->getActivePath(), + 'line' => $line, + 'char' => $char, + 'code' => $this->getLintMessageFullCode($code), + 'severity' => $this->getLintMessageSeverity($code), + 'name' => $this->getLintMessageName($code), + 'description' => $desc, + ); + + if ($original !== null) { + $dict['original'] = $original; + } + if ($replacement !== null) { + $dict['replacement'] = $replacement; + } + + return $this->addLintMessage(ArcanistLintMessage::newFromDictionary($dict)); + } + + protected function raiseLintAtPath( + $code, + $desc) { + + $path = $this->getActivePath(); + return $this->raiseLintAtLine(null, null, $code, $desc, null, null); + } + + protected function raiseLintAtOffset( + $offset, + $code, + $desc, + $original = null, + $replacement = null) { + + $path = $this->getActivePath(); + $engine = $this->getEngine(); + list($line, $char) = $engine->getLineAndCharFromOffset($path, $offset); + + return $this->raiseLintAtLine( + $line + 1, + $char + 1, + $code, + $desc, + $original, + $replacement); + } + + public function willLintPath($path) { + $this->stopAllLinters = false; + $this->activePath = $path; + } + + abstract public function willLintPaths(array $paths); + abstract public function lintPath($path); + abstract public function getLinterName(); + abstract public function getLintSeverityMap(); + abstract public function getLintNameMap(); + +} diff --git a/src/lint/linter/base/__init__.php b/src/lint/linter/base/__init__.php new file mode 100644 index 00000000..4e79e23c --- /dev/null +++ b/src/lint/linter/base/__init__.php @@ -0,0 +1,12 @@ + 'Bad Filename', + ); + } + + public function lintPath($path) { + if (!preg_match('@^[a-z0-9./_-]+$@i', $path)) { + $this->raiseLintAtPath( + self::LINT_BAD_FILENAME, + 'Name files using only letters, numbers, period, hyphen and '. + 'underscore.'); + } + } + +} diff --git a/src/lint/linter/filename/__init__.php b/src/lint/linter/filename/__init__.php new file mode 100644 index 00000000..6c515257 --- /dev/null +++ b/src/lint/linter/filename/__init__.php @@ -0,0 +1,21 @@ +getData($path); + + if (preg_match('/@generated/', $data)) { + $this->stopAllLinters(); + } + } + +} diff --git a/src/lint/linter/generated/__init__.php b/src/lint/linter/generated/__init__.php new file mode 100644 index 00000000..e325114a --- /dev/null +++ b/src/lint/linter/generated/__init__.php @@ -0,0 +1,12 @@ +getPEP8Options(); + + list($stdout) = execx( + "/usr/bin/env python2.6 %s {$options} %s", + $pep8_bin, + $this->getEngine()->getFilePathOnDisk($path)); + + $lines = explode("\n", $stdout); + $messages = array(); + foreach ($lines as $line) { + $matches = null; + if (!preg_match('/^(.*?):(\d+):(\d+): (\S+) (.*)$/', $line, $matches)) { + continue; + } + foreach ($matches as $key => $match) { + $matches[$key] = trim($match); + } + $message = new ArcanistLintMessage(); + $message->setPath($path); + $message->setLine($matches[2]); + $message->setChar($matches[3]); + $message->setCode($matches[4]); + $message->setName('PEP8 '.$matches[4]); + $message->setDescription($matches[5]); + if ($matches[4][0] == 'E') { + $message->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR); + } else { + $message->setSeveirty(ArcanistLintSeverity::SEVERITY_WARNING); + } + $this->addLintMessage($message); + } + } + +} diff --git a/src/lint/linter/pep8/__init__.php b/src/lint/linter/pep8/__init__.php new file mode 100644 index 00000000..34660432 --- /dev/null +++ b/src/lint/linter/pep8/__init__.php @@ -0,0 +1,17 @@ + 'Use of Undeclared Class', + self::LINT_UNDECLARED_FUNCTION => 'Use of Undeclared Function', + self::LINT_UNDECLARED_INTERFACE => 'Use of Undeclared Interface', + self::LINT_UNDECLARED_SOURCE => 'Use of Nonexistent File', + self::LINT_UNUSED_SOURCE => 'Unused Source', + self::LINT_UNUSED_MODULE => 'Unused Module', + self::LINT_INIT_REBUILD => 'Rebuilt __init__.php File', + self::LINT_UNKNOWN_CLASS => 'Unknown Class', + self::LINT_UNKNOWN_FUNCTION => 'Unknown Function', + self::LINT_ANALYZER_SIGNATURE => 'Analyzer: Bad Call Signature', + self::LINT_ANALYZER_DYNAMIC => 'Analyzer: Dynamic Dependency', + self::LINT_ANALYZER_NO_INIT => 'Analyzer: No __init__.php File', + self::LINT_ANALYZER_MULTIPLE_CLASSES + => 'Analyzer: File Declares Multiple Classes', + self::LINT_ANALYZER_CLASS_FILENAME + => 'Analyzer: Filename Does Not Match Class Declaration', + ); + } + + public function getLinterName() { + return 'PHU'; + } + + public function getLintSeverityMap() { + return array( + self::LINT_ANALYZER_DYNAMIC => ArcanistLintSeverity::SEVERITY_WARNING, + ); + } + + private $moduleInfo = array(); + private $unknownClasses = array(); + private $unknownFunctions = array(); + + private function setModuleInfo($key, array $info) { + $this->moduleInfo[$key] = $info; + } + + private function getModulePathOnDisk($key) { + $info = $this->moduleInfo[$key]; + return $info['root'].'/'.$info['module']; + } + + private function getModuleDisplayName($key) { + $info = $this->moduleInfo[$key]; + return $info['module']; + } + + private function isPhutilLibraryMetadata($path) { + $file = basename($path); + return !strncmp('__phutil_library_', $file, strlen('__phutil_library_')); + } + + public function willLintPaths(array $paths) { + + $modules = array(); + $moduleinfo = array(); + + $project_root = $this->getEngine()->getWorkingCopy()->getProjectRoot(); + + foreach ($paths as $path) { + $absolute_path = $project_root.'/'.$path; + $library_root = phutil_get_library_root_for_path($absolute_path); + if (!$library_root) { + continue; + } + if ($this->isPhutilLibraryMetadata($path)) { + continue; + } + $library_name = phutil_get_library_name_for_root($library_root); + if (!is_dir($path)) { + $path = dirname($path); + } + $path = Filesystem::resolvePath( + $path, + $project_root); + if ($path == $library_root) { + continue; + } + $module_name = Filesystem::readablePath($path, $library_root); + $module_key = $library_name.':'.$module_name; + if (empty($modules[$module_key])) { + $modules[$module_key] = $module_key; + $this->setModuleInfo($module_key, array( + 'library' => $library_name, + 'root' => $library_root, + 'module' => $module_name, + )); + } + } + + if (!$modules) { + return; + } + + $modules = array_keys($modules); + + $arc_root = phutil_get_library_root('arcanist'); + $bin = dirname($arc_root).'/scripts/phutil_analyzer.php'; + + $futures = array(); + foreach ($modules as $key) { + $disk_path = $this->getModulePathOnDisk($key); + $futures[$key] = new ExecFuture( + '%s %s', + $bin, + $disk_path); + } + + $requirements = array(); + foreach (Futures($futures) as $key => $future) { + $requirements[$key] = $future->resolveJSON(); + } + + $dependencies = array(); + $futures = array(); + foreach ($requirements as $key => $requirement) { + foreach ($requirement['messages'] as $message) { + list($where, $text, $code, $description) = $message; + if ($where) { + $where = array($where); + } + $this->raiseLintInModule( + $key, + $code, + $description, + $where, + $text); + } + + foreach ($requirement['requires']['module'] as $req_module => $where) { + if (isset($requirements[$req_module])) { + $dependencies[$req_module] = $requirements[$req_module]; + } else { + list($library_name, $module_name) = explode(':', $req_module); + $library_root = phutil_get_library_root($library_name); + $this->setModuleInfo($req_module, array( + 'library' => $library_name, + 'root' => $library_root, + 'module' => $module_name, + )); + $disk_path = $this->getModulePathOnDisk($req_module); + $futures[$req_module] = new ExecFuture( + '%s %s', + $bin, + $disk_path); + } + } + } + + foreach (Futures($futures) as $key => $future) { + $dependencies[$key] = $future->resolveJSON(); + } + + foreach ($requirements as $key => $spec) { + $deps = array_intersect_key( + $dependencies, + $spec['requires']['module']); + $this->lintModule($key, $spec, $deps); + } + } + + private function lintModule($key, $spec, $deps) { + $resolvable = array(); + $need_classes = array(); + $need_functions = array(); + $drop_modules = array(); + + $used = array(); + static $types = array( + 'class' => self::LINT_UNDECLARED_CLASS, + 'interface' => self::LINT_UNDECLARED_INTERFACE, + 'function' => self::LINT_UNDECLARED_FUNCTION, + ); + foreach ($types as $type => $lint_code) { + foreach ($spec['requires'][$type] as $name => $places) { + $declared = $this->checkDependency( + $type, + $name, + $deps); + if (!$declared) { + $module = $this->getModuleDisplayName($key); + $message = $this->raiseLintInModule( + $key, + $lint_code, + "Module '{$module}' uses {$type} '{$name}' but does not include ". + "any module which declares it.", + $places); + + if ($type == 'class' || $type == 'interface') { + $class_spec = PhutilLibraryMapRegistry::findClass( + $library = null, + $name); + if ($class_spec) { + if (phutil_autoload_class($name)) { + $resolvable[] = $message; + $need_classes[$name] = $class_spec; + } else { + if (empty($this->unknownClasses[$name])) { + $this->unknownClasses[$name] = true; + $library = $class_spec['library']; + $this->raiseLintInModule( + $key, + self::LINT_UNKNOWN_CLASS, + "Class '{$name}' exists in the library map for library ". + "'{$library}', but could not be loaded. You may need to ". + "rebuild the library map.", + $places); + } + } + } else { + if (empty($this->unknownClasses[$name])) { + $this->unknownClasses[$name] = true; + $this->raiseLintInModule( + $key, + self::LINT_UNKNOWN_CLASS, + "Class '{$name}' could not be found in any known library. ". + "You may need to rebuild the map for the library which ". + "contains it.", + $places); + } + } + } else { + $func_spec = PhutilLibraryMapRegistry::findFunction( + $library = null, + $name); + if ($func_spec) { + if (phutil_autoload_function($name)) { + $resolvable[] = $message; + $need_functions[$name] = $func_spec; + } else { + if (empty($this->unknownFunctions[$name])) { + $this->unknownFunctions[$name] = true; + $library = $func_spec['library']; + $this->raiseLintInModule( + $key, + self::LINT_UNKNOWN_FUNCTION, + "Function '{$name}' exists in the library map for library ". + "'{$library}', but could not be loaded. You may need to ". + "rebuild the library map.", + $places); + } + } + } else { + if (empty($this->unknownFunctions[$name])) { + $this->unknownFunctions[$name] = true; + $this->raiseLintInModule( + $key, + self::LINT_UNKNOWN_FUNCTION, + "Function '{$name}' could not be found in any known ". + "library. You may need to rebuild the map for the library ". + "which contains it.", + $places); + } + } + } + } + $used[$declared] = true; + } + } + + $unused = array_diff_key($deps, $used); + foreach ($unused as $unused_module_key => $ignored) { + $module = $this->getModuleDisplayName($key); + $unused_module = $this->getModuleDisplayName($unused_module_key); + $resolvable[] = $this->raiseLintInModule( + $key, + self::LINT_UNUSED_MODULE, + "Module '{$module}' requires module '{$unused_module}' but does not ". + "use anything it declares.", + $spec['requires']['module'][$unused_module_key]); + $drop_modules[] = $unused_module_key; + } + + foreach ($spec['requires']['source'] as $file => $where) { + if (empty($spec['declares']['source'][$file])) { + $module = $this->getModuleDisplayName($key); + $resolvable[] = $this->raiseLintInModule( + $key, + self::LINT_UNDECLARED_SOURCE, + "Module '{$module}' requires source '{$file}', but it does not ". + "exist.", + $where); + } + } + + foreach ($spec['declares']['source'] as $file => $ignored) { + if (empty($spec['requires']['source'][$file])) { + $module = $this->getModuleDisplayName($key); + $resolvable[] = $this->raiseLintInModule( + $key, + self::LINT_UNUSED_SOURCE, + "Module '{$module}' does not include source file '{$file}'.", + null); + } + } + + if ($resolvable) { + $new_file = $this->buildNewModuleInit( + $key, + $spec, + $need_classes, + $need_functions, + $drop_modules); + $init_path = $this->getModulePathOnDisk($key).'/__init__.php'; + $init_path = Filesystem::readablePath($init_path); + if (file_exists($init_path)) { + $old_file = Filesystem::readFile($init_path); + $this->willLintPath($init_path); + $message = $this->raiseLintAtOffset( + 0, + self::LINT_INIT_REBUILD, + "This regenerated phutil '__init__.php' file is suggested to ". + "address lint problems with static dependencies in the module.", + $old_file, + $new_file); + $message->setDependentMessages($resolvable); + } + } + } + + private function buildNewModuleInit( + $key, + $spec, + $need_classes, + $need_functions, + $drop_modules) { + + $init = array(); + $init[] = ' $class_spec) { + $modules[$class_spec['library'].':'.$class_spec['module']] = true; + } + + foreach ($need_functions as $need => $func_spec) { + $modules[$func_spec['library'].':'.$func_spec['module']] = true; + } + + ksort($modules); + + $last = null; + foreach ($modules as $module_key => $ignored) { + + if (is_array($ignored)) { + $in_init = false; + $in_file = false; + foreach ($ignored as $where) { + list($file, $line) = explode(':', $where); + if ($file == '__init__.php') { + $in_init = true; + } else { + $in_file = true; + } + } + if ($in_file && !$in_init) { + // If this is a runtime include, don't try to put it in the + // __init__ file. + continue; + } + } + + list($library, $module_name) = explode(':', $module_key); + if ($last != $library) { + $last = $library; + if ($last != null) { + $init[] = null; + } + } + + $library = "'".addcslashes($library, "'\\")."'"; + $module_name = "'".addcslashes($module_name, "'\\")."'"; + + $init[] = "phutil_require_module({$library}, {$module_name});"; + } + + $init[] = null; + $init[] = null; + + $files = array_keys($spec['declares']['source']); + sort($files); + + foreach ($files as $file) { + $file = "'".addcslashes($file, "'\\")."'"; + $init[] = "phutil_require_source({$file});"; + } + $init[] = null; + + return implode("\n", $init); + } + + private function checkDependency($type, $name, $deps) { + foreach ($deps as $key => $dep) { + if (isset($dep['declares'][$type][$name])) { + return $key; + } + } + return false; + } + + public function raiseLintInModule($key, $code, $desc, $places, $text = null) { + if ($places) { + foreach ($places as $place) { + list($file, $offset) = explode(':', $place); + $this->willLintPath( + Filesystem::readablePath($this->getModulePathOnDisk($key).'/'.$file)); + return $this->raiseLintAtOffset( + $offset, + $code, + $desc, + $text); + } + } else { + $this->willLintPath($this->getModuleDisplayName($key)); + return $this->raiseLintAtPath( + $code, + $desc); + } + } + + public function lintPath($path) { + return; + } + +} diff --git a/src/lint/linter/phutilmodule/__init__.php b/src/lint/linter/phutilmodule/__init__.php new file mode 100644 index 00000000..c4dddeca --- /dev/null +++ b/src/lint/linter/phutilmodule/__init__.php @@ -0,0 +1,19 @@ +maxLineLength = $new_length; + return $this; + } + + public function willLintPaths(array $paths) { + return; + } + + public function getLinterName() { + return 'TXT'; + } + + public function getLintSeverityMap() { + return array( + self::LINT_LINE_WRAP => ArcanistLintSeverity::SEVERITY_WARNING, + ); + } + + public function getLintNameMap() { + return array( + self::LINT_DOS_NEWLINE => 'DOS Newlines', + self::LINT_TAB_LITERAL => 'Tab Literal', + self::LINT_LINE_WRAP => 'Line Too Long', + self::LINT_EOF_NEWLINE => 'File Does Not End in Newline', + self::LINT_BAD_CHARSET => 'Bad Charset', + self::LINT_TRAILING_WHITESPACE => 'Trailing Whitespace', + ); + } + + public function lintPath($path) { + $this->lintNewlines($path); + $this->lintTabs($path); + + if ($this->didStopAllLinters()) { + return; + } + + $this->lintCharset($path); + + if ($this->didStopAllLinters()) { + return; + } + + $this->lintLineLength($path); + $this->lintEOFNewline($path); + $this->lintTrailingWhitespace($path); + } + + protected function lintNewlines($path) { + $pos = strpos($this->getData($path), "\r"); + if ($pos !== false) { + $this->raiseLintAtOffset( + $pos, + self::LINT_DOS_NEWLINE, + 'You must use ONLY Unix linebreaks ("\n") in source code.', + "\r"); + $this->stopAllLinters(); + } + } + + protected function lintTabs($path) { + $pos = strpos($this->getData($path), "\t"); + if ($pos !== false) { + $this->raiseLintAtOffset( + $pos, + self::LINT_TAB_LITERAL, + 'Configure your editor to use spaces for indentation.', + "\t"); + } + } + + protected function lintLineLength($path) { + $lines = explode("\n", $this->getData($path)); + + $width = $this->maxLineLength; + foreach ($lines as $line_idx => $line) { + if (strlen($line) > $width) { + $this->raiseLintAtLine( + $line_idx + 1, + 1, + self::LINT_LINE_WRAP, + 'This line is '.number_format(strlen($line)).' characters long, '. + 'but the convention is '.$width.' characters.', + $line); + } + } + } + + protected function lintEOFNewline($path) { + $data = $this->getData($path); + if (!strlen($data) || $data[strlen($data) - 1] != "\n") { + $this->raiseLintAtOffset( + strlen($data), + self::LINT_EOF_NEWLINE, + "Files must end in a newline.", + '', + "\n"); + } + } + + protected function lintCharset($path) { + $data = $this->getData($path); + + $matches = null; + $preg = preg_match_all( + '/[^\x09\x0A\x20-\x7E]+/', + $data, + $matches, + PREG_OFFSET_CAPTURE); + + if (!$preg) { + return; + } + + foreach ($matches[0] as $match) { + list($string, $offset) = $match; + $this->raiseLintAtOffset( + $offset, + self::LINT_BAD_CHARSET, + 'Source code should contain only ASCII bytes with ordinal decimal '. + 'values between 32 and 126 inclusive, plus linefeed. Do not use UTF-8 '. + 'or other multibyte charsets.', + $string); + } + + $this->stopAllLinters(); + } + + protected function lintTrailingWhitespace($path) { + $data = $this->getData($path); + + $matches = null; + $preg = preg_match_all( + '/ +$/m', + $data, + $matches, + PREG_OFFSET_CAPTURE); + + if (!$preg) { + return; + } + + foreach ($matches[0] as $match) { + list($string, $offset) = $match; + $this->raiseLintAtOffset( + $offset, + self::LINT_TRAILING_WHITESPACE, + 'This line contains trailing whitespace.', + $string, + ''); + } + } + +} diff --git a/src/lint/linter/text/__init__.php b/src/lint/linter/text/__init__.php new file mode 100644 index 00000000..f5ecba31 --- /dev/null +++ b/src/lint/linter/text/__init__.php @@ -0,0 +1,13 @@ + 'PHP Syntax Error!', + self::LINT_UNABLE_TO_PARSE => 'Unable to Parse', + self::LINT_VARIABLE_VARIABLE => 'Use of Variable Variable', + self::LINT_EXTRACT_USE => 'Use of extract()', + self::LINT_UNDECLARED_VARIABLE => 'Use of Undeclared Variable', + self::LINT_PHP_SHORT_TAG => 'Use of Short Tag " 'Use of Echo Tag " 'Use of Close Tag "?>"', + self::LINT_NAMING_CONVENTIONS => 'Naming Conventions', + self::LINT_IMPLICIT_CONSTRUCTOR => 'Implicit Constructor', + self::LINT_FORMATTING_CONVENTIONS => 'Formatting Conventions', + self::LINT_DYNAMIC_DEFINE => 'Dynamic define()', + self::LINT_STATIC_THIS => 'Use of $this in Static Context', + self::LINT_PREG_QUOTE_MISUSE => 'Misuse of preg_quote()', + self::LINT_PHP_OPEN_TAG => 'Expected Open Tag', + self::LINT_TODO_COMMENT => 'TODO Comment', + self::LINT_EXIT_EXPRESSION => 'Exit Used as Expression', + self::LINT_COMMENT_STYLE => 'Comment Style', + ); + } + + public function getLinterName() { + return 'XHP'; + } + + public function getLintSeverityMap() { + return array( + self::LINT_TODO_COMMENT => ArcanistLintSeverity::SEVERITY_ADVICE, + self::LINT_FORMATTING_CONVENTIONS + => ArcanistLintSeverity::SEVERITY_WARNING, + self::LINT_NAMING_CONVENTIONS + => ArcanistLintSeverity::SEVERITY_WARNING, + ); + } + + public function willLintPaths(array $paths) { + $futures = array(); + foreach ($paths as $path) { + $futures[$path] = xhpast_get_parser_future($this->getData($path)); + } + foreach ($futures as $path => $future) { + $this->willLintPath($path); + try { + $this->trees[$path] = XHPASTTree::newFromDataAndResolvedExecFuture( + $this->getData($path), + $future->resolve()); + } catch (XHPASTSyntaxErrorException $ex) { + $this->raiseLintAtLine( + $ex->getErrorLine(), + 1, + self::LINT_PHP_SYNTAX_ERROR, + 'This file contains a syntax error: '.$ex->getMessage()); + $this->stopAllLinters(); + return; + } catch (Exception $ex) { + $this->raiseLintAtPath( + self::LINT_UNABLE_TO_PARSE, + 'XHPAST could not parse this file, probably because the AST is too '. + 'deep. Some lint issues may not have been detected. You may safely '. + 'ignore this warning.'); + return; + } + } + } + + public function lintPath($path) { + if (empty($this->trees[$path])) { + return; + } + + $root = $this->trees[$path]->getRootNode(); + + $this->lintUseOfThisInStaticMethods($root); + $this->lintDynamicDefines($root); + $this->lintSurpriseConstructors($root); + $this->lintPHPTagUse($root); + $this->lintVariableVariables($root); + $this->lintTODOComments($root); + $this->lintExitExpressions($root); + $this->lintSpaceAroundBinaryOperators($root); + $this->lintSpaceAfterControlStatementKeywords($root); + $this->lintParenthesesShouldHugExpressions($root); + $this->lintNamingConventions($root); + $this->lintPregQuote($root); + $this->lintUndeclaredVariables($root); + $this->lintArrayIndexWhitespace($root); + $this->lintHashComments($root); + } + + protected function lintHashComments($root) { + $tokens = $root->getTokens(); + foreach ($tokens as $token) { + if ($token->getTypeName() == 'T_COMMENT') { + $value = $token->getValue(); + if ($value[0] == '#') { + $this->raiseLintAtOffset( + $token->getOffset(), + self::LINT_COMMENT_STYLE, + 'Use "//" single-line comments, not "#".', + '#', + '//'); + } + } + } + } + + protected function lintVariableVariables($root) { + $vvars = $root->selectDescendantsOfType('n_VARIABLE_VARIABLE'); + foreach ($vvars as $vvar) { + $this->raiseLintAtNode( + $vvar, + self::LINT_VARIABLE_VARIABLE, + 'Rewrite this code to use an array. Variable variables are unclear '. + 'and hinder static analysis.'); + } + } + + protected function lintUndeclaredVariables($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. + + $fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); + $mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION'); + $defs = $fdefs->add($mdefs); + + 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, + '$GLOBALS' => 0, + '$_SERVER' => 0, + '$_GET' => 0, + '$_POST' => 0, + '$_FILES' => 0, + '$_COOKIE' => 0, + '$_SESSION' => 0, + '$_REQUEST' => 0, + '$_ENV' => 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. + } + } + } + + $catches = $body + ->selectDescendantsOfType('n_CATCH') + ->selectDescendantsOfType('n_VARIABLE'); + foreach ($catches as $var) { + $vars[] = $var; + } + + $foreaches = $body->selectDescendantsOfType('n_FOREACH_EXPRESSION'); + foreach ($foreaches as $foreach_expr) { + $key_var = $foreach_expr->getChildByIndex(1); + if ($key_var->getTypeName() == 'n_VARIABLE') { + $vars[] = $key_var; + } + + $value_var = $foreach_expr->getChildByIndex(2); + if ($value_var->getTypeName() == 'n_VARIABLE') { + $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. + $vars[] = $value_var->getChildOfType(0, '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, + 'Avoid extract(). It is confusing and hinders static analysis.'); + } + + // Now we have every declaration. 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 = $var->getConcreteString(); + $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 uses, 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; + } + + // 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. + + $all_vars = $body->selectDescendantsOfType('n_VARIABLE'); + $issued_warnings = array(); + foreach ($all_vars as $var) { + 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; + } + if ($var->getOffset() >= $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; + } + $concrete = $var->getConcreteString(); + if ($var->getOffset() >= 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->raiseLintAtNode( + $var, + self::LINT_UNDECLARED_VARIABLE, + 'Declare variables prior to use (even if you are passing them '. + 'as reference parameters). You may have misspelled this '. + 'variable name.'); + $issued_warnings[$concrete] = true; + } + } + } + + protected function lintPHPTagUse($root) { + $tokens = $root->getTokens(); + foreach ($tokens as $token) { + if ($token->getTypeName() == 'T_OPEN_TAG') { + if (trim($token->getValue()) == 'raiseLintAtToken( + $token, + self::LINT_PHP_SHORT_TAG, + 'Use the full form of the PHP open tag, "getTypeName() == 'T_OPEN_TAG_WITH_ECHO') { + $this->raiseLintAtToken( + $token, + self::LINT_PHP_ECHO_TAG, + 'Avoid the PHP echo short form, "getValue())) { + $this->raiseLintAtToken( + $token, + self::LINT_PHP_OPEN_TAG, + 'PHP files should start with "getTypeName() == 'T_CLOSE_TAG') { + $this->raiseLintAtToken( + $token, + self::LINT_PHP_CLOSE_TAG, + 'Do not use the PHP closing tag, "?>".'); + } + } + } + + protected function lintNamingConventions($root) { + $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); + foreach ($classes as $class) { + $name_token = $class->getChildByIndex(1); + $name_string = $name_token->getConcreteString(); + $is_xhp = ($name_string[0] == ':'); + if ($is_xhp) { + if (!$this->isLowerCaseWithXHP($name_string)) { + $this->raiseLintAtNode( + $name_token, + self::LINT_NAMING_CONVENTIONS, + 'Follow naming conventions: xhp elements should be named using '. + 'lower case.'); + } + } else { + if (!$this->isUpperCamelCase($name_string)) { + $this->raiseLintAtNode( + $name_token, + self::LINT_NAMING_CONVENTIONS, + '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(); + if (!$this->isUpperCamelCase($name_string)) { + $this->raiseLintAtNode( + $name_token, + self::LINT_NAMING_CONVENTIONS, + '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(); + if (!$this->isLowercaseWithUnderscores($name_string)) { + $this->raiseLintAtNode( + $name_token, + self::LINT_NAMING_CONVENTIONS, + '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(); + if (!$this->isLowerCamelCase($name_string)) { + $this->raiseLintAtNode( + $name_token, + self::LINT_NAMING_CONVENTIONS, + 'Follow naming conventions: methods should be named using '. + 'lowerCamelCase.'); + } + } + + + $params = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST'); + foreach ($params as $param_list) { + foreach ($param_list->getChildren() as $param) { + $name_token = $param->getChildByIndex(1); + $name_string = $name_token->getConcreteString(); + if (!$this->isLowercaseWithUnderscores($name_string)) { + $this->raiseLintAtNode( + $name_token, + self::LINT_NAMING_CONVENTIONS, + '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(); + if (!$this->isUppercaseWithUnderscores($name_string)) { + $this->raiseLintAtNode( + $name_token, + self::LINT_NAMING_CONVENTIONS, + 'Follow naming conventions: class constants should be named using '. + 'UPPERCASE_WITH_UNDERSCORES.'); + } + } + } + + $props = $root->selectDescendantsOfType('n_CLASS_MEMBER_DECLARATION_LIST'); + foreach ($props as $prop_list) { + foreach ($prop_list->getChildren() as $prop) { + if ($prop->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') { + continue; + } + $name_token = $prop->getChildByIndex(0); + $name_string = $name_token->getConcreteString(); + if (!$this->isLowerCamelCase($name_string)) { + $this->raiseLintAtNode( + $name_token, + self::LINT_NAMING_CONVENTIONS, + 'Follow naming conventions: class properties should be named '. + 'using lowerCamelCase.'); + } + } + } + } + + protected function isUpperCamelCase($str) { + return preg_match('/^[A-Z][A-Za-z0-9]*$/', $str); + } + + protected function isLowerCamelCase($str) { + // Allow initial "__" for magic methods like __construct; we could also + // enumerate these explicitly. + return preg_match('/^\$?(?:__)?[a-z][A-Za-z0-9]*$/', $str); + } + + protected function isUppercaseWithUnderscores($str) { + return preg_match('/^[A-Z0-9_]+$/', $str); + } + + protected function isLowercaseWithUnderscores($str) { + return preg_match('/^[&]?\$?[a-z0-9_]+$/', $str); + } + + protected function isLowercaseWithXHP($str) { + return preg_match('/^:[a-z0-9_:-]+$/', $str); + } + + protected function lintSurpriseConstructors($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, + 'Name constructors __construct() explicitly. This method is a '. + 'constructor because it has the same name as the class it is '. + 'defined in.'); + } + } + } + } + + protected function lintParenthesesShouldHugExpressions($root) { + $calls = $root->selectDescendantsOfType('n_CALL_PARAMETER_LIST'); + $controls = $root->selectDescendantsOfType('n_CONTROL_CONDITION'); + $fors = $root->selectDescendantsOfType('n_FOR_EXPRESSION'); + $foreach = $root->selectDescendantsOfType('n_FOREACH_EXPRESSION'); + $decl = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST'); + + $all_paren_groups = $calls + ->add($controls) + ->add($fors) + ->add($foreach) + ->add($decl); + 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('Expected open paren!'); + } + if ($token_c->getTypeName() != ')') { + throw new Exception('Expected close paren!'); + } + + $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_FORMATTING_CONVENTIONS, + 'Parentheses should hug their contents.', + $string, + ''); + } + } + } + + protected function lintSpaceAfterControlStatementKeywords($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_FORMATTING_CONVENTIONS, + 'Convention: put a space after control statements.', + $token->getValue().' '); + } + break; + } + } + } + + protected function lintSpaceAroundBinaryOperators($root) { + $expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION'); + foreach ($expressions as $expression) { + $operator = $expression->getChildByIndex(1); + $operator_value = $operator->getConcreteString(); + if ($operator_value == '.') { + // TODO: implement this check + continue; + } else { + 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_FORMATTING_CONVENTIONS, + 'Convention: logical and arithmetic operators should be '. + 'surrounded by whitespace.', + $replace); + } + } + } + } + + protected function lintDynamicDefines($root) { + $calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); + foreach ($calls as $call) { + $name = $call->getChildByIndex(0)->getConcreteString(); + if (strtolower($name) == 'define') { + $parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); + $defined = $parameter_list->getChildByIndex(0); + if (!$defined->isStaticScalar()) { + $this->raiseLintAtNode( + $defined, + self::LINT_DYNAMIC_DEFINE, + 'First argument to define() must be a string literal.'); + } + } + } + } + + protected function lintUseOfThisInStaticMethods($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, + 'You can not reference "$this" inside a static method.'); + } + } + } + } + } + + /** + * preg_quote() takes two arguments, but the second one is optional because + * PHP is awesome. If you don't pass a second argument, you're probably + * going to get something wrong. + */ + protected function lintPregQuote($root) { + $function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); + foreach ($function_calls as $call) { + $name = $call->getChildByIndex(0)->getConcreteString(); + if (strtolower($name) === 'preg_quote') { + $parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST'); + if (count($parameter_list->getChildren()) !== 2) { + $this->raiseLintAtNode( + $call, + self::LINT_PREG_QUOTE_MISUSE, + 'You should always pass two arguments to preg_quote(), so that ' . + 'preg_quote() knows which delimiter to escape.'); + } + } + } + } + + /** + * 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! + */ + protected function lintExitExpressions($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, + "Use exit as a statement, not an expression."); + } + } + } + } + + private function lintArrayIndexWhitespace($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_FORMATTING_CONVENTIONS, + 'Convention: no spaces before index access.', + $trailing_text, + ''); + } + } + } + + protected function lintTODOComments($root) { + $tokens = $root->getTokens(); + foreach ($tokens as $token) { + if (!$token->isComment()) { + continue; + } + + $value = $token->getValue(); + $matches = null; + $preg = preg_match_all( + '/TODO/', + $value, + $matches, + PREG_OFFSET_CAPTURE); + + foreach ($matches[0] as $match) { + list($string, $offset) = $match; + $this->raiseLintAtOffset( + $token->getOffset() + $offset, + self::LINT_TODO_COMMENT, + 'This comment has a TODO.', + $string); + } + } + } + + protected function raiseLintAtToken( + XHPASTToken $token, + $code, + $desc, + $replace = null) { + return $this->raiseLintAtOffset( + $token->getOffset(), + $code, + $desc, + $token->getValue(), + $replace); + } + + protected function raiseLintAtNode( + XHPASTNode $node, + $code, + $desc, + $replace = null) { + return $this->raiseLintAtOffset( + $node->getOffset(), + $code, + $desc, + $node->getConcreteString(), + $replace); + } + +} diff --git a/src/lint/linter/xhpast/__init__.php b/src/lint/linter/xhpast/__init__.php new file mode 100644 index 00000000..364f4d66 --- /dev/null +++ b/src/lint/linter/xhpast/__init__.php @@ -0,0 +1,17 @@ +lintFile($root.$file); + } + } + + private function lintFile($file) { + $working_copy = ArcanistWorkingCopyIdentity::newFromPath(__FILE__); + + $contents = Filesystem::readFile($file); + $contents = explode("~~~~~~~~~~\n", $contents); + if (count($contents) < 2) { + throw new Exception( + "Expected '~~~~~~~~~~' separating test case and results."); + } + + list ($data, $expect, $xform, $config) = array_merge( + $contents, + array(null, null)); + + $basename = basename($file); + + if ($config) { + $config = json_decode($config, true); + if (!is_array($config)) { + throw new Exception( + "Invalid configuration in test '{$basename}', not valid JSON."); + } + } else { + $config = array(); + } + + /* TODO: ? + validate_parameter_list( + $config, + array( + ), + array( + 'project' => true, + 'path' => true, + 'hook' => true, + )); + */ + + $exception = null; + $after_lint = null; + $messages = null; + $exception_message = false; + $caught_exception = false; + try { + + $path = idx($config, 'path', 'lint/'.$basename.'.php'); + + $engine = new UnitTestableArcanistLintEngine(); + $engine->setWorkingCopy($working_copy); + $engine->setPaths(array($path)); + +// TODO: restore this +// $engine->setCommitHookMode(idx($config, 'hook', false)); + + $linter = new ArcanistXHPASTLinter(); + $linter->addPath($path); + $linter->addData($path, $data); + + $engine->addLinter($linter); + $engine->addFileData($path, $data); + + $results = $engine->run(); + $this->assertEqual( + 1, + count($results), + 'Expect one result returned by linter.'); + + $result = reset($results); + $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result); + $after_lint = $patcher->getModifiedFileContent(); + + } catch (ArcanistPhutilTestTerminatedException $ex) { + throw $ex; + } catch (Exception $exception) { + $caught_exception = true; + $exception_message = $exception->getMessage()."\n\n". + $exception->getTraceAsString(); + } + + switch ($basename) { + default: + $this->assertEqual(false, $caught_exception, $exception_message); + $this->compareLint($basename, $expect, $result); + $this->compareTransform($xform, $after_lint); + break; + } + } + + private function compareLint($file, $expect, $result) { + $seen = array(); + $raised = array(); + foreach ($result->getMessages() as $message) { + $sev = $message->getSeverity(); + $line = $message->getLine(); + $char = $message->getChar(); + $code = $message->getCode(); + $name = $message->getName(); + $seen[] = $sev.":".$line.":".$char; + $raised[] = " {$sev} at line {$line}, char {$char}: {$code} {$name}"; + } + $expect = trim($expect); + if ($expect) { + $expect = explode("\n", $expect); + } else { + $expect = array(); + } + foreach ($expect as $key => $expected) { + $expect[$key] = reset(explode(' ', $expected)); + } + + $expect = array_fill_keys($expect, true); + $seen = array_fill_keys($seen, true); + + if (!$raised) { + $raised = array("No messages."); + } + $raised = "Actually raised:\n".implode("\n", $raised); + + foreach (array_diff_key($expect, $seen) as $missing => $ignored) { + list($sev, $line, $char) = explode(':', $missing); + $this->assertFailure( + "In '{$file}', ". + "expected lint to raise {$sev} on line {$line} at char {$char}, ". + "but no {$sev} was raised. {$raised}"); + } + + foreach (array_diff_key($seen, $expect) as $surprising => $ignored) { + list($sev, $line, $char) = explode(':', $surprising); + $this->assertFailure( + "In '{$file}', ". + "lint raised {$sev} on line {$line} at char {$char}, ". + "but nothing was expected. {$raised}"); + } + } + + private function compareTransform($expected, $actual) { + if (!strlen($expected)) { + return; + } + $this->assertEqual( + $expected, + $actual, + "File as patched by lint did not match the expected patched file."); + } +} diff --git a/src/lint/linter/xhpast/__tests__/__init__.php b/src/lint/linter/xhpast/__tests__/__init__.php new file mode 100644 index 00000000..b4fa3967 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/__init__.php @@ -0,0 +1,19 @@ +define($pony, $cute); +~~~~~~~~~~ +error:3:8 dynamic define +error:5:8 dynamic define diff --git a/src/lint/linter/xhpast/__tests__/data/embedded-tags.lint-test b/src/lint/linter/xhpast/__tests__/data/embedded-tags.lint-test new file mode 100644 index 00000000..b0da8c13 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/embedded-tags.lint-test @@ -0,0 +1,5 @@ + + +This shouldn't fatal the parser. +~~~~~~~~~~ +error:1:10 \ No newline at end of file diff --git a/src/lint/linter/xhpast/__tests__/data/exit-expression.lint-test b/src/lint/linter/xhpast/__tests__/data/exit-expression.lint-test new file mode 100644 index 00000000..55e8b048 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/exit-expression.lint-test @@ -0,0 +1,8 @@ + + x + + { + $x + + } + + + + & + + ' + + " + + + + + ; + +$x+$y; +~~~~~~~~~~ +warning:39:3 +~~~~~~~~~~ + + x + + { + $x + + } + + + + & + + ' + + " + + + + + ; + +$x + $y; diff --git a/src/lint/linter/xhpast/__tests__/data/hash-comments.lint-test b/src/lint/linter/xhpast/__tests__/data/hash-comments.lint-test new file mode 100644 index 00000000..3c5d700d --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/hash-comments.lint-test @@ -0,0 +1,34 @@ +const $y;; + +class :ui:lol-whatever:omg { } + +function () use ($this_is_a_closure) { }; + +function f(&$YY) { +} +~~~~~~~~~~ +warning:2:7 +warning:3:9 +warning:3:16 +warning:4:13 +warning:4:17 +warning:5:19 +warning:5:21 +warning:5:25 +warning:8:11 +warning:10:7 +warning:12:10 +warning:12:13 +warning:26:12 \ No newline at end of file diff --git a/src/lint/linter/xhpast/__tests__/data/no-segfault-on-abstract.lint-test b/src/lint/linter/xhpast/__tests__/data/no-segfault-on-abstract.lint-test new file mode 100644 index 00000000..9c642f96 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/no-segfault-on-abstract.lint-test @@ -0,0 +1,4 @@ +( a b c ); +$obj->m( + $x, + $y, + $z); +for ( $ii = 0; $ii < 1; $ii++ ) { } +foreach ( $x as $y ) { } +function q( $x ) { } +class X { + public function f( $y ) { + } +} +foreach ( $z as $k => $v ) { +} +some_call( /* respect authorial intent */ $b, + $a // if comments are present + ); +~~~~~~~~~~ +warning:2:5 +warning:2:8 +warning:3:3 +warning:4:3 +warning:7:10 +warning:7:13 +warning:13:6 +warning:13:30 +warning:14:10 +warning:14:19 +warning:15:12 +warning:15:15 +warning:17:21 +warning:17:24 +warning:20:10 +warning:20:25 +~~~~~~~~~~ +( a b c ); +$obj->m( + $x, + $y, + $z); +for ($ii = 0; $ii < 1; $ii++) { } +foreach ($x as $y) { } +function q($x) { } +class X { + public function f($y) { + } +} +foreach ($z as $k => $v) { +} +some_call( /* respect authorial intent */ $b, + $a // if comments are present + ); diff --git a/src/lint/linter/xhpast/__tests__/data/php-tags-bad.lint-test b/src/lint/linter/xhpast/__tests__/data/php-tags-bad.lint-test new file mode 100644 index 00000000..7f71ef9f --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/php-tags-bad.lint-test @@ -0,0 +1,14 @@ +garbage garbage + +~~~~~~~~~~ +error:1:1 +error:2:1 +error:4:1 +~~~~~~~~~~ +garbage garbage + diff --git a/src/lint/linter/xhpast/__tests__/data/php-tags-echo-form.lint-test b/src/lint/linter/xhpast/__tests__/data/php-tags-echo-form.lint-test new file mode 100644 index 00000000..414db072 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/php-tags-echo-form.lint-test @@ -0,0 +1,4 @@ +if; +~~~~~~~~~~ +warning:2:1 +warning:3:1 +warning:4:1 +warning:5:1 +warning:6:1 +warning:7:1 +warning:7:6 +warning:8:1 +~~~~~~~~~~ +if; diff --git a/src/lint/linter/xhpast/__tests__/data/space-around-operators.lint-test b/src/lint/linter/xhpast/__tests__/data/space-around-operators.lint-test new file mode 100644 index 00000000..31aa35f0 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/space-around-operators.lint-test @@ -0,0 +1,59 @@ +1+1=2; +$y /* ! */ += // ? +$z; +$obj->method(); +Dog::bark(); +$prev = ($total == 1) ? $navids[0] : $navids[$total-1]; +$next = ($total == 1) ? $navids[0] : $navids[$current+1]; +if ($x instanceof :y:z &&$w) { } +if ($x instanceof :y:z && $w) { } +~~~~~~~~~~ +warning:3:3 +warning:4:4 +warning:5:3 +warning:7:3 +warning:8:3 +warning:9:4 +warning:10:3 +warning:11:3 +warning:20:52 +warning:21:54 +warning:22:24 +~~~~~~~~~~ +1+1=2; +$y /* ! */ += // ? +$z; +$obj->method(); +Dog::bark(); +$prev = ($total == 1) ? $navids[0] : $navids[$total - 1]; +$next = ($total == 1) ? $navids[0] : $navids[$current + 1]; +if ($x instanceof :y:z && $w) { } +if ($x instanceof :y:z && $w) { } diff --git a/src/lint/linter/xhpast/__tests__/data/surprising-constructors.lint-test b/src/lint/linter/xhpast/__tests__/data/surprising-constructors.lint-test new file mode 100644 index 00000000..229683f5 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/surprising-constructors.lint-test @@ -0,0 +1,8 @@ + $m) { + + } + + $a++; + $b++; + $c++; + $d++; + $e++; + $f++; + $g++; + $h++; + $i++; + $j++; + $k++; + $l++; + $m++; + $this++; + $n++; // Only one that isn't declared. + + extract(z()); + + $o++; +} + +function g($q) { + $$q = x(); + $r = y(); +} + +class C { + public function m() { + $a++; + x($b); + $c[] = 3; + $d->v = 4; + $a = $f; + } +} + +function worst() { + global $$x; + $y++; +} + +function superglobals() { + $GLOBALS[$_FILES[$_POST[$this]]]++; +} + +function ref_foreach($x) { + foreach ($x as &$z) { + + } + $z++; +} + +function has_default($x = 0) { + $x++; +} + +function declparse( + $a, + Q $b, + Q &$c, + Q $d = null, + Q &$e = null, + $f, + $g = null, + &$h, + &$i = null) { + $a++; + $b++; + $c++; + $d++; + $e++; + $f++; + $g++; + $h++; + $i++; + $j++; +} + +function declparse_a(Q $a) { $a++; } +function declparse_b(Q &$a) { $a++; } +function declparse_c(Q $a = null) { $a++; } +function declparse_d(Q &$a = null) { $a++; } +function declparse_e($a) { $a++; } +function declparse_f(&$a) { $a++; } +function declparse_g($a = null) { $a++; } +function declparse_h(&$a = null) { $a++; } + +function static_class() { + SomeClass::$x; +} + +function instance_class() { + $a = $this->$x; +} + +function exception_vars() { + try { + } catch (Exception $y) { + $y++; + } +} + +function nonuse() { + isset($x); + empty($y); + $x++; +} + +function twice() { + $y++; + $y++; +} + +function more_exceptions() { + try { + } catch (Exception $a) { + $a++; + } catch (Exception $b) { + $b++; + } +} + +class P { + abstract public function q(); +} + +function x() { + $lib = $_SERVER['PHP_ROOT'].'/lib/titan/display/read/init.php'; + require_once($lib); + f(((($lib)))); // Tests for paren expressions. + f(((($lub)))); +} + +class A { + public function foo($a) { + $im_service = foo($a); + if ($im_servce === false) { + return 1; + } + return 2; + } +} + + +~~~~~~~~~~ +error:30:3 +error:32:3 +error:38:3 +error:44:5 +error:45:7 +error:46:5 +error:47:5 +error:48:10 +error:53:10 worst ever +error:91:3 This stuff is basically testing the lexer/parser for function decls. +error:108:15 Variables in instance derefs should be checked, static should not. +error:121:3 isset() and empty() should not trigger errors. +error:125:3 Should only warn once in this function. +error:146:8 +error:152:9 \ No newline at end of file diff --git a/src/lint/linter/xhpast/__tests__/data/unreasonably-deep-nesting.lint-test b/src/lint/linter/xhpast/__tests__/data/unreasonably-deep-nesting.lint-test new file mode 100644 index 00000000..42a39c41 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/unreasonably-deep-nesting.lint-test @@ -0,0 +1,86 @@ +500) of string concatenations. We emit n_CONCATENATION_LIST instead of +// n_BINARY_EXPRESSION to avoid various call-depth traps in PHP, HPHP, and the +// builtin JSON decoder. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''; +~~~~~~~~~~ +~~~~~~~~~~ +500) of string concatenations. We emit n_CONCATENATION_LIST instead of +// n_BINARY_EXPRESSION to avoid various call-depth traps in PHP, HPHP, and the +// builtin JSON decoder. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''. +''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''; diff --git a/src/lint/linter/xhpast/__tests__/data/use-of-this-in-static-method.lint-test b/src/lint/linter/xhpast/__tests__/data/use-of-this-in-static-method.lint-test new file mode 100644 index 00000000..1d340a55 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/use-of-this-in-static-method.lint-test @@ -0,0 +1,13 @@ +f(); + } + public static function v() { + $this->f(); + } +} + +~~~~~~~~~~ +error:8:5 Use of $this in a static method. diff --git a/src/lint/linter/xhpast/__tests__/data/variable-variables.lint-test b/src/lint/linter/xhpast/__tests__/data/variable-variables.lint-test new file mode 100644 index 00000000..d3e9a2f1 --- /dev/null +++ b/src/lint/linter/xhpast/__tests__/data/variable-variables.lint-test @@ -0,0 +1,5 @@ +$bar; // okay +~~~~~~~~~~ +error:2:1 diff --git a/src/lint/message/ArcanistLintMessage.php b/src/lint/message/ArcanistLintMessage.php new file mode 100644 index 00000000..fd938786 --- /dev/null +++ b/src/lint/message/ArcanistLintMessage.php @@ -0,0 +1,169 @@ +setPath($dict['path']); + $message->setLine($dict['line']); + $message->setChar($dict['char']); + $message->setCode($dict['code']); + $message->setSeverity($dict['severity']); + $message->setName($dict['name']); + $message->setDescription($dict['description']); + if (isset($dict['original'])) { + $message->setOriginalText($dict['original']); + } + if (isset($dict['replacement'])) { + $message->setReplacementText($dict['replacement']); + } + return $message; + } + + public function setPath($path) { + $this->path = $path; + return $this; + } + + public function getPath() { + return $this->path; + } + + public function setLine($line) { + $this->line = $line; + return $this; + } + + public function getLine() { + return $this->line; + } + + public function setChar($char) { + $this->char = $char; + return $this; + } + + public function getChar() { + return $this->char; + } + + public function setCode($code) { + $this->code = $code; + return $this; + } + + public function getCode() { + return $this->code; + } + + public function setSeverity($severity) { + $this->severity = $severity; + return $this; + } + + public function getSeverity() { + return $this->severity; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setDescription($description) { + $this->description = $description; + return $this; + } + + public function getDescription() { + return $this->description; + } + + public function setOriginalText($original) { + $this->originalText = $original; + return $this; + } + + public function getOriginalText() { + return $this->originalText; + } + + public function setReplacementText($replacement) { + $this->replacementText = $replacement; + return $this; + } + + public function getReplacementText() { + return $this->replacementText; + } + + public function isError() { + return $this->getSeverity() == ArcanistLintSeverity::SEVERITY_ERROR; + } + + public function isWarning() { + return $this->getSeverity() == ArcanistLintSeverity::SEVERITY_WARNING; + } + + public function hasFileContext() { + return ($this->getLine() !== null); + } + + public function isPatchable() { + return ($this->getReplacementText() !== null); + } + + public function didApplyPatch() { + if ($this->appliedToDisk) { + return; + } + $this->appliedToDisk = true; + foreach ($this->dependentMessages as $message) { + $message->didApplyPatch(); + } + return $this; + } + + public function isPatchApplied() { + return $this->appliedToDisk; + } + + public function setDependentMessages(array $messages) { + $this->dependentMessages = $messages; + return $this; + } + +} diff --git a/src/lint/message/__init__.php b/src/lint/message/__init__.php new file mode 100644 index 00000000..f40b8bd6 --- /dev/null +++ b/src/lint/message/__init__.php @@ -0,0 +1,11 @@ +lintResult = $result; + return $obj; + } + + public function getUnmodifiedFileContent() { + return $this->lintResult->getData(); + } + + public function getModifiedFileContent() { + if ($this->modifiedData === null) { + $this->buildModifiedFile(); + } + return $this->modifiedData; + } + + public function writePatchToDisk() { + $path = $this->lintResult->getFilePathOnDisk(); + $data = $this->getModifiedFileContent(); + + $ii = null; + do { + $lint = $path.'.linted'.($ii++); + } while (file_exists($lint)); + + // Copy existing file to preserve permissions. 'chmod --reference' is not + // supported under OSX. + execx('cp -p %s %s', $path, $lint); + Filesystem::writeFile($lint, $data); + + list($err) = exec_manual("mv -f %s %s", $lint, $path); + if ($err) { + throw new Exception( + "Unable to overwrite path `{$path}', patched version was left ". + "at `{$lint}'."); + } + + foreach ($this->applyMessages as $message) { + $message->didApplyPatch(); + } + } + + private function __construct() { + + } + + private function buildModifiedFile() { + $data = $this->getUnmodifiedFileContent(); + + foreach ($this->lintResult->getMessages() as $lint) { + if (!$lint->isPatchable()) { + continue; + } + + $orig_offset = $this->getCharacterOffset($lint->getLine() - 1); + $orig_offset += $lint->getChar() - 1; + + $dirty = $this->getDirtyCharacterOffset(); + if ($dirty > $orig_offset) { + continue; + } + + // Adjust the character offset by the delta *after* checking for + // dirtiness. The dirty character cursor is a cursor on the original file, + // and should be compared with the patch position in the original file. + $working_offset = $orig_offset + $this->getCharacterDelta(); + + $old_str = $lint->getOriginalText(); + $old_len = strlen($old_str); + $new_str = $lint->getReplacementText(); + $new_len = strlen($new_str); + + $data = substr_replace($data, $new_str, $working_offset, $old_len); + + $this->changeCharacterDelta($new_len - $old_len); + $this->setDirtyCharacterOffset($orig_offset + $old_len); + + $this->applyMessages[] = $lint; + } + + $this->modifiedData = $data; + } + + private function getCharacterOffset($line_num) { + if ($this->lineOffsets === null) { + $lines = explode("\n", $this->getUnmodifiedFileContent()); + $this->lineOffsets = array(0); + $last = 0; + foreach ($lines as $line) { + $this->lineOffsets[] = $last + strlen($line) + 1; + $last += strlen($line) + 1; + } + } + + if ($line_num >= count($this->lineOffsets)) { + throw new Exception("Data has fewer than `{$line}' lines."); + } + + return idx($this->lineOffsets, $line_num); + } + + private function setDirtyCharacterOffset($offset) { + $this->dirtyUntil = $offset; + return $this; + } + + private function getDirtyCharacterOffset() { + return $this->dirtyUntil; + } + + private function changeCharacterDelta($change) { + $this->characterDelta += $change; + return $this; + } + + private function getCharacterDelta() { + return $this->characterDelta; + } + +} diff --git a/src/lint/patcher/__init__.php b/src/lint/patcher/__init__.php new file mode 100644 index 00000000..7b5ef263 --- /dev/null +++ b/src/lint/patcher/__init__.php @@ -0,0 +1,14 @@ +summaryMode = $mode; + } + + public function renderLintResult(ArcanistLintResult $result) { + if ($this->summaryMode) { + return $this->renderResultSummary($result); + } else { + return $this->renderResultFull($result); + } + } + + protected function renderResultFull(ArcanistLintResult $result) { + $messages = $result->getMessages(); + $path = $result->getPath(); + $lines = explode("\n", $result->getData()); + + $text = array(); + $text[] = phutil_console_format('**>>>** Lint for __%s__:', $path); + $text[] = null; + foreach ($messages as $message) { + if ($message->isError()) { + $color = 'red'; + } else { + $color = 'yellow'; + } + + $severity = ArcanistLintSeverity::getStringForSeverity( + $message->getSeverity()); + $code = $message->getCode(); + $name = $message->getName(); + $description = phutil_console_wrap($message->getDescription(), 4); + + $text[] = phutil_console_format( + " ** %s ** (%s) __%s__\n". + " %s\n", + $severity, + $code, + $name, + $description); + + if ($message->hasFileContext()) { + $text[] = $this->renderContext($message, $lines); + } + } + $text[] = null; + $text[] = null; + + return implode("\n", $text); + } + + protected function renderResultSummary(ArcanistLintResult $result) { + $messages = $result->getMessages(); + $path = $result->getPath(); + + $text = array(); + $text[] = $path.":"; + foreach ($messages as $message) { + $name = $message->getName(); + $severity = ArcanistLintSeverity::getStringForSeverity( + $message->getSeverity()); + $line = $message->getLine(); + + $text[] = " {$severity} on line {$line}: {$name}"; + } + $text[] = null; + + return implode("\n", $text); + } + + + protected function renderContext( + ArcanistLintMessage $message, + array $line_data) { + + $lines_of_context = 3; + $out = array(); + + $line_num = min($message->getLine(), count($line_data)); + $line_num = max(1, $line_num); + + // Print out preceding context before the impacted region. + $cursor = max(1, $line_num - $lines_of_context); + for (; $cursor < $line_num; $cursor++) { + $out[] = $this->renderLine($cursor, $line_data[$cursor - 1]); + } + + // Print out the impacted region itself. + $diff = $message->isPatchable() ? '-' : null; + $text = $message->getOriginalText(); + $text_lines = explode("\n", $text); + $text_length = count($text_lines); + + for (; $cursor < $line_num + $text_length; $cursor++) { + $chevron = ($cursor == $line_num); + $data = $line_data[$cursor - 1]; + + // Highlight the problem substring. + $text_line = $text_lines[$cursor - $line_num]; + if (strlen($text_line)) { + $data = substr_replace( + $data, + phutil_console_format('##%s##', $text_line), + ($cursor == $line_num) + ? $message->getChar() - 1 + : 0, + strlen($text_line)); + } + + $out[] = $this->renderLine($cursor, $data, $chevron, $diff); + } + + if ($message->isPatchable()) { + $patch = $message->getReplacementText(); + $patch_lines = explode("\n", $patch); + $offset = 0; + foreach ($patch_lines as $patch_line) { + if (isset($line_data[$line_num - 1 + $offset])) { + $base = $line_data[$line_num - 1 + $offset]; + } else { + $base = ''; + } + + if ($offset == 0) { + $start = $message->getChar() - 1; + } else { + $start = 0; + } + + if (isset($text_lines[$offset])) { + $len = strlen($text_lines[$offset]); + } else { + $len = 0; + } + + $patched = substr_replace( + $base, + phutil_console_format('##%s##', $patch_line), + $start, + $len); + $out[] = $this->renderLine(null, $patched, false, '+'); + + $offset++; + } + } + + $lines_count = count($line_data); + $end = min($lines_count, $cursor + $lines_of_context); + for (; $cursor < $end; $cursor++) { + $out[] = $this->renderLine($cursor, $line_data[$cursor - 1]); + } + $out[] = null; + + return implode("\n", $out); + } + + protected function renderLine($line, $data, $chevron = false, $diff = null) { + $chevron = $chevron ? '>>>' : ''; + return sprintf( + " %3s %1s %6s %s", + $chevron, + $diff, + $line, + $data); + } + +} diff --git a/src/lint/renderer/__init__.php b/src/lint/renderer/__init__.php new file mode 100644 index 00000000..c89f46b8 --- /dev/null +++ b/src/lint/renderer/__init__.php @@ -0,0 +1,14 @@ +path = $path; + return $this; + } + + public function getPath() { + return $this->path; + } + + public function addMessage(ArcanistLintMessage $message) { + $this->messages[] = $message; + $this->needsSort = true; + return $this; + } + + public function getMessages() { + if ($this->needsSort) { + $this->sortMessages(); + } + return $this->messages; + } + + public function setData($data) { + $this->data = $data; + return $this; + } + + public function getData() { + return $this->data; + } + + public function setFilePathOnDisk($file_path_on_disk) { + $this->filePathOnDisk = $file_path_on_disk; + return $this; + } + + public function getFilePathOnDisk() { + return $this->filePathOnDisk; + } + + public function isPatchable() { + foreach ($this->messages as $message) { + if ($message->isPatchable()) { + return true; + } + } + return false; + } + + private function sortMessages() { + $messages = $this->messages; + $map = array(); + foreach ($messages as $key => $message) { + $map[$key] = ($message->getLine() * (2 << 12)) + $message->getChar(); + } + asort($map); + $messages = array_select_keys($messages, array_keys($map)); + $this->messages = $messages; + $this->needsSort = false; + + return $this; + } + +} diff --git a/src/lint/result/__init__.php b/src/lint/result/__init__.php new file mode 100644 index 00000000..419d48e5 --- /dev/null +++ b/src/lint/result/__init__.php @@ -0,0 +1,12 @@ + 'Advice', + self::SEVERITY_WARNING => 'Warning', + self::SEVERITY_ERROR => 'Error', + self::SEVERITY_DISABLED => 'Disabled', + ); + + if (!array_key_exists($severity_code, $map)) { + throw new Exception("Unknown lint severity '{$severity_code}'!"); + } + + return $map[$severity_code]; + } + + public static function isAtLeastAsSevere( + ArcanistLintMessage $message, + $level) { + + static $map = array( + self::SEVERITY_DISABLED => 10, + self::SEVERITY_ADVICE => 20, + self::SEVERITY_WARNING => 30, + self::SEVERITY_ERROR => 40, + ); + + $message_sev = $message->getSeverity(); + if (empty($map[$message_sev])) { + return true; + } + + return $map[$message_sev] >= idx($map, $level, 0); + } + + +} diff --git a/src/lint/severity/__init__.php b/src/lint/severity/__init__.php new file mode 100644 index 00000000..b68772f1 --- /dev/null +++ b/src/lint/severity/__init__.php @@ -0,0 +1,12 @@ +changes = $changes; + return $obj; + } + + public static function newFromArcBundle($path) { + $path = Filesystem::resolvePath($path); + + $future = new ExecFuture( + csprintf( + 'tar xfO %s changes.json', + $path)); + $changes = $future->resolveJSON(); + + foreach ($changes as $change_key => $change) { + foreach ($change['hunks'] as $key => $hunk) { + list($hunk_data) = execx('tar xfO %s hunks/%s', $path, $hunk['corpus']); + $changes[$change_key]['hunks'][$key]['corpus'] = $hunk_data; + } + } + + foreach ($changes as $change_key => $change) { + $changes[$change_key] = ArcanistDiffChange::newFromDictionary($change); + } + + $obj = new ArcanistBundle(); + $obj->changes = $changes; + + return $obj; + } + + public static function newFromDiff($data) { + $obj = new ArcanistBundle(); + + $parser = new ArcanistDiffParser(); + $obj->changes = $parser->parseDiff($data); + + return $obj; + } + + private function __construct() { + + } + + public function writeToDisk($path) { + $changes = $this->getChanges(); + + $change_list = array(); + foreach ($changes as $change) { + $change_list[] = $change->toDictionary(); + } + + $hunks = array(); + foreach ($change_list as $change_key => $change) { + foreach ($change['hunks'] as $key => $hunk) { + $hunks[] = $hunk['corpus']; + $change_list[$change_key]['hunks'][$key]['corpus'] = count($hunks) - 1; + } + } + + $blobs = array(); + + $dir = Filesystem::createTemporaryDirectory(); + Filesystem::createDirectory($dir.'/hunks'); + Filesystem::createDirectory($dir.'/blobs'); + Filesystem::writeFile($dir.'/changes.json', json_encode($change_list)); + foreach ($hunks as $key => $hunk) { + Filesystem::writeFile($dir.'/hunks/'.$key, $hunk); + } + foreach ($blobs as $key => $blob) { + Filesystem::writeFile($dir.'/blobs/'.$key, $blob); + } + execx( + '(cd %s; tar -czf %s *)', + $dir, + Filesystem::resolvePath($path)); + Filesystem::remove($dir); + } + + public function toUnifiedDiff() { + + $result = array(); + $changes = $this->getChanges(); + foreach ($changes as $change) { + + $old_path = $this->getOldPath($change); + $cur_path = $this->getCurrentPath($change); + + $index_path = $cur_path; + if ($index_path === null) { + $index_path = $old_path; + } + + $result[] = 'Index: '.$index_path; + $result[] = str_repeat('=', 67); + + if ($old_path === null) { + $old_path = '/dev/null'; + } + + if ($cur_path === null) { + $cur_path = '/dev/null'; + } + + $result[] = '--- '.$old_path; + $result[] = '+++ '.$cur_path; + + $result[] = $this->buildHunkChanges($change->getHunks()); + } + + return implode("\n", $result)."\n"; + } + + public function toGitPatch() { + $result = array(); + $changes = $this->getChanges(); + foreach ($changes as $change) { + $type = $change->getType(); + $file_type = $change->getFileType(); + + if ($file_type == ArcanistDiffChangeType::FILE_DIRECTORY) { + // TODO: We should raise a FYI about this, so the user is aware + // that we omitted it, if the directory is empty or has permissions + // which git can't represent. + + // Git doesn't support empty directories, so we simply ignore them. If + // the directory is nonempty, 'git apply' will create it when processing + // the changesets for files inside it. + continue; + } + + if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) { + // Git will apply this in the corresponding MOVE_HERE. + continue; + } + + $old_mode = idx($change->getOldProperties(), 'unix:filemode', '100644'); + $new_mode = idx($change->getNewProperties(), 'unix:filemode', '100644'); + + $change_body = $this->buildHunkChanges($change->getHunks()); + if ($type == ArcanistDiffChangeType::TYPE_COPY_AWAY) { + // TODO: This is only relevant when patching old Differential diffs + // which were created prior to arc pruning TYPE_COPY_AWAY for files + // with no modifications. + if (!strlen($change_body) && ($old_mode == $new_mode)) { + continue; + } + } + + $old_path = $this->getOldPath($change); + $cur_path = $this->getCurrentPath($change); + + if ($old_path === null) { + $old_index = 'a/'.$cur_path; + $old_target = '/dev/null'; + } else { + $old_index = 'a/'.$old_path; + $old_target = 'a/'.$old_path; + } + + if ($cur_path === null) { + $cur_index = 'b/'.$old_path; + $cur_target = '/dev/null'; + } else { + $cur_index = 'b/'.$cur_path; + $cur_target = 'b/'.$cur_path; + } + + $result[] = "diff --git {$old_index} {$cur_index}"; + + if ($type == ArcanistDiffChangeType::TYPE_ADD) { + $result[] = "new file mode {$new_mode}"; + } + + if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE || + $type == ArcanistDiffChangeType::TYPE_MOVE_HERE || + $type == ArcanistDiffChangeType::TYPE_COPY_AWAY) { + if ($old_mode !== $new_mode) { + $result[] = "old mode {$old_mode}"; + $result[] = "new mode {$new_mode}"; + } + } + + if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE) { + $result[] = "copy from {$old_path}"; + $result[] = "copy to {$cur_path}"; + } else if ($type == ArcanistDiffChangeType::TYPE_MOVE_HERE) { + $result[] = "rename from {$old_path}"; + $result[] = "rename to {$cur_path}"; + } else if ($type == ArcanistDiffChangeType::TYPE_DELETE || + $type == ArcanistDiffChangeType::TYPE_MULTICOPY) { + $old_mode = idx($change->getOldProperties(), 'unix:filemode'); + if ($old_mode) { + $result[] = "deleted file mode {$old_mode}"; + } + } + + $result[] = "--- {$old_target}"; + $result[] = "+++ {$cur_target}"; + $result[] = $change_body; + } + return implode("\n", $result)."\n"; + } + + public function getChanges() { + return $this->changes; + } + + private function breakHunkIntoSmallHunks(ArcanistDiffHunk $hunk) { + $context = 3; + + $results = array(); + $lines = explode("\n", $hunk->getCorpus()); + $n = count($lines); + + $old_offset = $hunk->getOldOffset(); + $new_offset = $hunk->getNewOffset(); + + $ii = 0; + $jj = 0; + while ($ii < $n) { + for ($jj = $ii; $jj < $n && $lines[$jj][0] == ' '; ++$jj) { + // Skip lines until we find the first line with changes. + } + if ($jj >= $n) { + break; + } + + $hunk_start = max($jj - $context, 0); + + $old_lines = 0; + $new_lines = 0; + $last_change = $jj; + for (; $jj < $n; ++$jj) { + if ($lines[$jj][0] == ' ') { + if ($jj - $last_change > $context) { + break; + } + } else { + $last_change = $jj; + if ($lines[$jj][0] == '-') { + ++$old_lines; + } else { + ++$new_lines; + } + } + } + + $hunk_length = min($jj, $n) - $hunk_start; + + $hunk = new ArcanistDiffHunk(); + $hunk->setOldOffset($old_offset + $hunk_start - $ii); + $hunk->setNewOffset($new_offset + $hunk_start - $ii); + $hunk->setOldLength($hunk_length - $new_lines); + $hunk->setNewLength($hunk_length - $old_lines); + + $corpus = array_slice($lines, $hunk_start, $hunk_length); + $corpus = implode("\n", $corpus); + $hunk->setCorpus($corpus); + + $results[] = $hunk; + + $old_offset += ($jj - $ii) - $new_lines; + $new_offset += ($jj - $ii) - $old_lines; + $ii = $jj; + } + + return $results; + } + + private function getOldPath(ArcanistDiffChange $change) { + $old_path = $change->getOldPath(); + $type = $change->getType(); + + if (!strlen($old_path) || + $type == ArcanistDiffChangeType::TYPE_ADD) { + $old_path = null; + } + + return $old_path; + } + + private function getCurrentPath(ArcanistDiffChange $change) { + $cur_path = $change->getCurrentPath(); + $type = $change->getType(); + + if (!strlen($cur_path) || + $type == ArcanistDiffChangeType::TYPE_DELETE || + $type == ArcanistDiffChangeType::TYPE_MULTICOPY) { + $cur_path = null; + } + + return $cur_path; + } + + private function buildHunkChanges(array $hunks) { + $result = array(); + foreach ($hunks as $hunk) { + $small_hunks = $this->breakHunkIntoSmallHunks($hunk); + foreach ($small_hunks as $small_hunk) { + $o_off = $small_hunk->getOldOffset(); + $o_len = $small_hunk->getOldLength(); + $n_off = $small_hunk->getNewOffset(); + $n_len = $small_hunk->getNewLength(); + $corpus = $small_hunk->getCorpus(); + + $result[] = "@@ -{$o_off},{$o_len} +{$n_off},{$n_len} @@"; + $result[] = $corpus; + } + } + return implode("\n", $result); + } + +} diff --git a/src/parser/bundle/__init__.php b/src/parser/bundle/__init__.php new file mode 100644 index 00000000..7ab00fb2 --- /dev/null +++ b/src/parser/bundle/__init__.php @@ -0,0 +1,20 @@ +api = $api; + return $this; + } + + protected function getRepositoryAPI() { + return $this->api; + } + + public function setDetectBinaryFiles($detect) { + $this->detectBinaryFiles = $detect; + return $this; + } + + public function parseSubversionDiff(ArcanistSubversionAPI $api, $paths) { + $this->setRepositoryAPI($api); + + $diffs = array(); + + foreach ($paths as $path => $status) { + if ($status & ArcanistRepositoryAPI::FLAG_UNTRACKED || + $status & ArcanistRepositoryAPI::FLAG_CONFLICT || + $status & ArcanistRepositoryAPI::FLAG_MISSING) { + unset($paths[$path]); + } + } + + $root = null; + $from = array(); + foreach ($paths as $path => $status) { + $change = $this->buildChange($path); + + if ($status & ArcanistRepositoryAPI::FLAG_ADDED) { + $change->setType(ArcanistDiffChangeType::TYPE_ADD); + } else if ($status & ArcanistRepositoryAPI::FLAG_DELETED) { + $change->setType(ArcanistDiffChangeType::TYPE_DELETE); + } else { + $change->setType(ArcanistDiffChangeType::TYPE_CHANGE); + } + + $is_dir = is_dir($api->getPath($path)); + if ($is_dir) { + $change->setFileType(ArcanistDiffChangeType::FILE_DIRECTORY); + // We have to go hit the diff even for directories because they may + // have property changes or moves, etc. + } + $is_link = is_link($api->getPath($path)); + if ($is_link) { + $change->setFileType(ArcanistDiffChangeType::FILE_SYMLINK); + } + + $diff = $api->getRawDiffText($path); + if ($diff) { + $this->parseDiff($diff); + } + + $info = $api->getSVNInfo($path); + if (idx($info, 'Copied From URL')) { + if (!$root) { + $rinfo = $api->getSVNInfo('.'); + $root = $rinfo['URL'].'/'; + } + $cpath = $info['Copied From URL']; + $cpath = substr($cpath, strlen($root)); + $change->setOldPath($cpath); + + $from[$path] = $cpath; + } + } + + foreach ($paths as $path => $status) { + $change = $this->buildChange($path); + if (empty($from[$path])) { + continue; + } + + if (empty($this->changes[$from[$path]])) { + if ($change->getType() == ArcanistDiffChangeType::TYPE_COPY_HERE) { + // If the origin path wasn't changed (or isn't included in this diff) + // and we only copied it, don't generate a changeset for it. This + // keeps us out of trouble when we go to 'arc commit' and need to + // figure out which files should be included in the commit list. + continue; + } + } + + $origin = $this->buildChange($from[$path]); + $origin->addAwayPath($change->getCurrentPath()); + + $type = $origin->getType(); + switch ($type) { + case ArcanistDiffChangeType::TYPE_MULTICOPY: + case ArcanistDiffChangeType::TYPE_COPY_AWAY: + break; + case ArcanistDiffChangeType::TYPE_DELETE: + $origin->setType(ArcanistDiffChangeType::TYPE_MOVE_AWAY); + break; + case ArcanistDiffChangeType::TYPE_MOVE_AWAY: + $origin->setType(ArcanistDiffChangeType::TYPE_MULTICOPY); + break; + case ArcanistDiffChangeType::TYPE_CHANGE: + $origin->setType(ArcanistDiffChangeType::TYPE_COPY_AWAY); + break; + default: + throw new Exception("Bad origin state {$type}."); + } + + $type = $origin->getType(); + switch ($type) { + case ArcanistDiffChangeType::TYPE_MULTICOPY: + case ArcanistDiffChangeType::TYPE_MOVE_AWAY: + $change->setType(ArcanistDiffChangeType::TYPE_MOVE_HERE); + break; + case ArcanistDiffChangeType::TYPE_COPY_AWAY: + $change->setType(ArcanistDiffChangeType::TYPE_COPY_HERE); + break; + default: + throw new Exception("Bad origin state {$type}."); + } + } + + return $this->changes; + } + + public function parseDiff($diff) { + $this->didStartParse($diff); + + if ($this->getLine() === null) { + $this->didFailParse("Can't parse an empty diff!"); + } + + do { + $patterns = array( + // This is a normal SVN text change, probably from "svn diff". + '(?Index): (?.+)', + // This is an SVN property change, probably from "svn diff". + '(?Property changes on): (?.+)', + // This is a git commit message, probably from "git show". + '(?commit) (?[a-f0-9]+)', + // This is a git diff, probably from "git show" or "git diff". + '(?diff --git) a/(?.+) b/(?.+)', + // This is a unified diff, probably from "diff -u" or synthetic diffing. + '(?---) (?.+)\s+\d{4}-\d{2}-\d{2}.*', + '(?Binary) files '. + '(?.+)\s+\d{4}-\d{2}-\d{2} and '. + '(?.+)\s+\d{4}-\d{2}-\d{2} differ.*', + ); + + $ok = false; + $line = $this->getLine(); + $match = null; + foreach ($patterns as $pattern) { + $ok = preg_match('@^'.$pattern.'$@', $line, $match); + if ($ok) { + break; + } + } + + if (!$ok) { + $this->didFailParse( + "Expected a hunk header, like 'Index: /path/to/file.ext' (svn), ". + "'Property changes on: /path/to/file.ext' (svn properties), ". + "'commit 59bcc3ad6775562f845953cf01624225' (git show), ". + "'diff --git' (git diff), or '--- filename' (unified diff)."); + } + + $change = $this->buildChange(idx($match, 'cur')); + + if (isset($match['old'])) { + $change->setOldPath($match['old']); + } + + if (isset($match['hash'])) { + $change->setCommitHash($match['hash']); + } + + if (isset($match['binary'])) { + $change->setFileType(ArcanistDiffChangeType::FILE_BINARY); + $line = $this->nextNonemptyLine(); + continue; + } + + $line = $this->nextLine(); + + switch ($match['type']) { + case 'Index': + $this->parseIndexHunk($change); + break; + case 'Property changes on': + $this->parsePropertyHunk($change); + break; + case 'diff --git': + $this->setIsGit(true); + $this->parseIndexHunk($change); + break; + case 'commit': + $this->setIsGit(true); + $this->parseCommitMessage($change); + break; + case '---': + $ok = preg_match( + '@^(?:\+\+\+) (.*)\s+\d{4}-\d{2}-\d{2}.*$@', + $line, + $match); + if (!$ok) { + $this->didFailParse("Expected '+++ filename' in unified diff."); + } + $change->setCurrentPath($match[1]); + $line = $this->nextLine(); + $this->parseChangeset($change); + break; + default: + $this->didFailParse("Unknown diff type."); + } + } while ($this->getLine() !== null); + + $this->didFinishParse(); + + return $this->changes; + } + + protected function parseCommitMessage(ArcanistDiffChange $change) { + $change->setType(ArcanistDiffChangeType::TYPE_MESSAGE); + + $message = array(); + + $line = $this->getLine(); + if (preg_match('/^Merge: /', $line)) { + $this->nextLine(); + } + + $line = $this->getLine(); + if (!preg_match('/^Author: /', $line)) { + $this->didFailParse("Expected 'Author:'."); + } + + $line = $this->nextLine(); + if (!preg_match('/^Date: /', $line)) { + $this->didFailParse("Expected 'Date:'."); + } + + while (($line = $this->nextLine()) !== null) { + if (strlen($line) && $line[0] != ' ') { + break; + } + // Strip leading spaces from Git commit messages. + $message[] = substr($line, 4); + } + + $message = rtrim(implode("\n", $message)); + $change->setMetadata('message', $message); + } + + /** + * Parse an SVN property change hunk. These hunks are ambiguous so just sort + * of try to get it mostly right. It's entirely possible to foil this parser + * (or any other parser) with a carefully constructed property change. + */ + protected function parsePropertyHunk(ArcanistDiffChange $change) { + $line = $this->getLine(); + if (!preg_match('/^_+$/', $line)) { + $this->didFailParse("Expected '______________________'."); + } + + $line = $this->nextLine(); + while ($line !== null) { + $done = preg_match('/^(Index|Property changes on):/', $line); + if ($done) { + break; + } + + $matches = null; + $ok = preg_match('/^(Modified|Added|Deleted): (.*)$/', $line, $matches); + if (!$ok) { + $this->didFailParse("Expected 'Added', 'Deleted', or 'Modified'."); + } + + $op = $matches[1]; + $prop = $matches[2]; + + list($old, $new) = $this->parseSVNPropertyChange($op, $prop); + + if ($old !== null) { + $change->setOldProperty($prop, $old); + } + + if ($new !== null) { + $change->setNewProperty($prop, $new); + } + + $line = $this->getLine(); + } + } + + private function parseSVNPropertyChange($op, $prop) { + + $old = array(); + $new = array(); + + $target = null; + + $line = $this->nextLine(); + while ($line !== null) { + $done = preg_match( + '/^(Modified|Added|Deleted|Index|Property changes on):/', + $line); + if ($done) { + break; + } + $trimline = ltrim($line); + if ($trimline && $trimline[0] == '+') { + if ($op == 'Deleted') { + $this->didFailParse('Unexpected "+" section in property deletion.'); + } + $target = 'new'; + $line = substr($trimline, 2); + } else if ($trimline && $trimline[0] == '-') { + if ($op == 'Added') { + $this->didFailParse('Unexpected "-" section in property addition.'); + } + $target = 'old'; + $line = substr($trimline, 2); + } else if (!strncmp($trimline, 'Merged', 6)) { + if ($op == 'Added') { + $target = 'new'; + } else { + // These can appear on merges. No idea how to interpret this (unclear + // what the old / new values are) and it's of dubious usefulness so + // just throw it away until someone complains. + $target = null; + } + $line = $trimline; + } + + if ($target == 'new') { + $new[] = $line; + } else if ($target == 'old') { + $old[] = $line; + } + + $line = $this->nextLine(); + } + + $old = rtrim(implode("\n", $old)); + $new = rtrim(implode("\n", $new)); + + if (!strlen($old)) { + $old = null; + } + + if (!strlen($new)) { + $new = null; + } + + return array($old, $new); + } + + protected function setIsGit($git) { + if ($this->isGit !== null && $this->isGit != $git) { + throw new Exception("Git status has changed!"); + } + $this->isGit = $git; + return $this; + } + + protected function getIsGit() { + return $this->isGit; + } + + protected function parseIndexHunk(ArcanistDiffChange $change) { + $is_git = $this->getIsGit(); + + $line = $this->getLine(); + if ($is_git) { + do { + + $patterns = array( + '(?new) file mode (?\d+)', + '(?deleted) file mode (?\d+)', + // These occur when someone uses `chmod` on a file. + 'old mode (?\d+)', + 'new mode (?\d+)', + // These occur when you `mv` a file and git figures it out. + 'similarity index ', + 'rename from (?.*)', + '(?rename) to (?.*)', + 'copy from (?.*)', + '(?copy) to (?.*)' + ); + + $ok = false; + $match = null; + foreach ($patterns as $pattern) { + $ok = preg_match('@^'.$pattern.'@', $line, $match); + if ($ok) { + break; + } + } + + if (!$ok) { + if ($line === null || + preg_match('/^(diff --git|commit) /', $line)) { + // In this case, there are ONLY file mode changes, or this is a + // pure move. + return; + } + break; + } + + if (!empty($match['oldmode'])) { + $change->setOldProperty('unix:filemode', $match['oldmode']); + } + if (!empty($match['newmode'])) { + $change->setNewProperty('unix:filemode', $match['newmode']); + } + + if (!empty($match['deleted'])) { + $change->setType(ArcanistDiffChangeType::TYPE_DELETE); + } + + if (!empty($match['new'])) { + $change->setType(ArcanistDiffChangeType::TYPE_ADD); + } + + if (!empty($match['old'])) { + $change->setOldPath($match['old']); + } + + if (!empty($match['cur'])) { + $change->setCurrentPath($match['cur']); + } + + if (!empty($match['copy'])) { + $change->setType(ArcanistDiffChangeType::TYPE_COPY_HERE); + $old = $this->buildChange($change->getOldPath()); + $type = $old->getType(); + + if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) { + $old->setType(ArcanistDiffChangeType::TYPE_MULTICOPY); + } else { + $old->setType(ArcanistDiffChangeType::TYPE_COPY_AWAY); + } + + $old->addAwayPath($change->getCurrentPath()); + } + + if (!empty($match['move'])) { + $change->setType(ArcanistDiffChangeType::TYPE_MOVE_HERE); + $old = $this->buildChange($change->getOldPath()); + $type = $old->getType(); + + if ($type == ArcanistDiffChangeType::TYPE_MULTICOPY) { + // Great, no change. + } else if ($type == ArcanistDiffChangeType::TYPE_MOVE_AWAY) { + $old->setType(ArcanistDiffChangeType::TYPE_MULTICOPY); + } else if ($type == ArcanistDiffChangeType::TYPE_COPY_AWAY) { + $old->setType(ArcanistDiffChangeType::TYPE_MULTICOPY); + } else { + $old->setType(ArcanistDiffChangeType::TYPE_MOVE_AWAY); + } + + $old->addAwayPath($change->getCurrentPath()); + } + + $line = $this->nextNonemptyLine(); + } while (true); + } + + $line = $this->getLine(); + $ok = preg_match('/^=+$/', $line) || + ($is_git && preg_match('/^index .*$/', $line)); + if (!$ok) { + if ($is_git) { + $this->didFailParse( + "Expected 'index af23f...a98bc' header line."); + } else { + $this->didFailParse( + "Expected '==========================' divider line."); + } + } + // Adding an empty file in SVN can produce an empty line here. + $line = $this->nextNonemptyLine(); + + // If there are files with only whitespace changes and -b or -w are + // supplied as command-line flags to `diff', svn and git both produce + // changes without any body. + if ($line === null || + preg_match( + '/^(Index:|Property changes on:|diff --git|commit) /', + $line)) { + return; + } + + $is_binary_add = preg_match( + '/^Cannot display: file marked as a binary type.$/', + $line); + if ($is_binary_add) { + $this->nextLine(); // Cannot display: file marked as a binary type. + $this->nextNonemptyLine(); // svn:mime-type = application/octet-stream + $this->pullBinaries($change); + return; + } + + // We can get this in git, or in SVN when a file exists in the repository + // WITHOUT a binary mime-type and is changed and given a binary mime-type. + $is_binary_diff = preg_match( + '/^Binary files .* and .* differ$/', + $line); + if ($is_binary_diff) { + $this->nextNonemptyLine(); // Binary files x and y differ + $this->pullBinaries($change); + return; + } + + if ($is_git) { + // "git diff -b" ignores whitespace, but has an empty hunk target + if (preg_match('@^diff --git a/.*$@', $line)) { + $this->nextLine(); + return null; + } + } + + $old_file = $this->parseHunkTarget(); + $new_file = $this->parseHunkTarget(); + + $change->setOldPath($old_file); + + $this->parseChangeset($change); + } + + protected function parseHunkTarget() { + $line = $this->getLine(); + $matches = null; + $ok = preg_match( + '@^[-+]{3} (?:[ab]/)?(?.*?)(?:\s*\(.*\))?$@', + $line, + $matches); + if (!$ok) { + $this->didFailParse( + "Expected hunk target '+++ path/to/file.ext (revision N)'."); + } + $this->nextLine(); + return $matches['path']; + } + + protected function pullBinaries(ArcanistDiffChange $change) { + $change->setFileType(ArcanistDiffChangeType::FILE_BINARY); + + // TODO: Reimplement this. + return; + +/* + $api = $this->getRepositoryAPI(); + if (!$api) { + return; + } + + $is_image = Filesystem::isImageFilename($change->getCurrentPath()); + if (!$is_image) { + // TODO: We could store binaries for reasonably-sized files. + return; + } + + $change->setFileType(ArcanistDiffChangeType::FILE_IMAGE); + + $old_data = $api->getOriginalFileData($change->getCurrentPath()); + $new_data = $api->getCurrentFileData($change->getCurrentPath()); + + $old_fbid = $this->createAttachment($change->getOldPath(), $old_data); + $new_fbid = $this->createAttachment($change->getCurrentPath(), $new_data); + + $info = array( + 'tools-attachment-old-fbid' => $old_fbid, + 'tools-attachment-new-fbid' => $new_fbid, + ); + + $change->setMetadata('attachment-data', $info); + +*/ + } + + protected function createAttachment($name, $data) { + // TODO: Implement attachments over conduit. + return null; + } + + protected function parseChangeset(ArcanistDiffChange $change) { + $all_changes = array(); + do { + $hunk = new ArcanistDiffHunk(); + $line = $this->getLine(); + $real = array(); + + // In the case where only one line is changed, the length is omitted. + // The final group is for git, which appends a guess at the function + // context to the diff. + $matches = null; + $ok = preg_match( + '/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*?)?$/U', + $line, + $matches); + + if (!$ok) { + $this->didFailParse("Expected hunk header '@@ -NN,NN +NN,NN @@'."); + } + + $hunk->setOldOffset($matches[1]); + $hunk->setNewOffset($matches[3]); + + // Cover for the cases where length wasn't present (implying one line). + $old_len = idx($matches, 2); + if (!strlen($old_len)) { + $old_len = 1; + } + $new_len = idx($matches, 4); + if (!strlen($new_len)) { + $new_len = 1; + } + + $hunk->setOldLength($old_len); + $hunk->setNewLength($new_len); + + $add = 0; + $del = 0; + + $advance = false; + while ((($line = $this->nextLine()) !== null)) { + if (strlen($line)) { + $char = $line[0]; + } else { + $char = '~'; + } + switch ($char) { + case '\\': + if (!preg_match('@\\ No newline at end of file@', $line)) { + $this->didFailParse( + "Expected '\ No newline at end of file'."); + } + if ($new_len) { + $hunk->setIsMissingOldNewline(true); + } else { + $hunk->setIsMissingNewNewline(true); + } + if (!$new_len) { + $advance = true; + break 2; + } + break; + case '+': + if (!$new_len) { + break 2; + } + ++$add; + --$new_len; + $real[] = $line; + break; + case '-': + if (!$old_len) { + break 2; + } + ++$del; + --$old_len; + $real[] = $line; + break; + case ' ': + if (!$old_len && !$new_len) { + break 2; + } + --$old_len; + --$new_len; + $real[] = $line; + break; + case '~': + $advance = true; + break 2; + default: + break 2; + } + } + + if ($old_len != 0 || $new_len != 0) { + $this->didFailParse("Found the wrong number of hunk lines."); + } + + $corpus = implode("\n", $real); + + $is_binary = false; + if ($this->detectBinaryFiles) { + $is_binary = preg_match('/([^\x09\x0A\x20-\x7E]+)/', $corpus); + } + + if ($is_binary) { + // SVN happily treats binary files which aren't marked with the right + // mime type as text files. Detect that junk here and mark the file + // binary. We'll catch stuff with unicode too, but that's verboten + // anyway. If there are too many false positives with this we might + // need to make it threshold-triggered instead of triggering on any + // unprintable byte. + $change->setFileType(ArcanistDiffChangeType::FILE_BINARY); + } else { + $hunk->setCorpus($corpus); + $hunk->setAddLines($add); + $hunk->setDelLines($del); + $change->addHunk($hunk); + } + + if ($advance) { + $line = $this->nextNonemptyLine(); + } + + } while (preg_match('/^@@ /', $line)); + } + + protected function buildChange($path = null) { + $change = null; + if ($path !== null) { + if (!empty($this->changes[$path])) { + return $this->changes[$path]; + } + } + + $change = new ArcanistDiffChange(); + if ($path !== null) { + $change->setCurrentPath($path); + $this->changes[$path] = $change; + } else { + $this->changes[] = $change; + } + + return $change; + } + + protected function didStartParse($text) { + // TODO: Removed an fb_utf8ize() call here. -epriestley + + // Eat leading whitespace. This may happen if the first change in the diff + // is an SVN property change. + $text = ltrim($text); + + $this->text = explode("\n", $text); + $this->line = 0; + } + + protected function getLine() { + if ($this->text === null) { + throw new Exception("Not parsing!"); + } + if (isset($this->text[$this->line])) { + return $this->text[$this->line]; + } + return null; + } + + protected function nextLine() { + $this->line++; + return $this->getLine(); + } + + protected function nextNonemptyLine() { + while (($line = $this->nextLine()) !== null) { + if (strlen(trim($line)) !== 0) { + break; + } + } + return $this->getLine(); + } + + protected function didFinishParse() { + $this->text = null; + } + + protected function didFailParse($message) { + $min = max(0, $this->line - 3); + $max = min($this->line + 3, count($this->text) - 1); + + $context = ''; + for ($ii = $min; $ii <= $max; $ii++) { + $context .= sprintf( + "%8.8s %s\n", + ($ii == $this->line) ? '>>> ' : '', + $this->text[$ii]); + } + + $message = "Parse Exception: {$message}\n\n{$context}\n"; + throw new Exception($message); + } +} diff --git a/src/parser/diff/__init__.php b/src/parser/diff/__init__.php new file mode 100644 index 00000000..58f3eb33 --- /dev/null +++ b/src/parser/diff/__init__.php @@ -0,0 +1,17 @@ +parseDiff($root.$file); + } + } + + private function parseDiff($diff_file) { + $contents = Filesystem::readFile($diff_file); + $file = basename($diff_file); + + $parser = new ArcanistDiffParser(); + $changes = $parser->parseDiff($contents); + + switch ($file) { + case 'basic-missing-both-newlines-plus.udiff': + case 'basic-missing-both-newlines.udiff': + case 'basic-missing-new-newline-plus.udiff': + case 'basic-missing-new-newline.udiff': + case 'basic-missing-old-newline-plus.udiff': + case 'basic-missing-old-newline.udiff': + $expect_old = strpos($file, '-old-') || strpos($file, '-both-'); + $expect_new = strpos($file, '-new-') || strpos($file, '-both-'); + $expect_two = strpos($file, '-plus'); + + $this->assertEqual(count($changes), $expect_two ? 2 : 1); + $change = reset($changes); + $this->assertEqual(true, $change !== null); + + $hunks = $change->getHunks(); + $this->assertEqual(1, count($hunks)); + + $hunk = reset($hunks); + $this->assertEqual((bool)$expect_old, $hunk->getIsMissingOldNewline()); + $this->assertEqual((bool)$expect_new, $hunk->getIsMissingNewNewline()); + break; + case 'basic-binary.udiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $this->assertEqual( + ArcanistDiffChangeType::FILE_BINARY, + $change->getFileType()); + break; + case 'basic-multi-hunk.udiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $hunks = $change->getHunks(); + $this->assertEqual(4, count($hunks)); + $this->assertEqual('right', $change->getCurrentPath()); + $this->assertEqual('left', $change->getOldPath()); + break; + case 'basic-multi-hunk-content.svndiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $hunks = $change->getHunks(); + $this->assertEqual(2, count($hunks)); + + $there_is_a_literal_trailing_space_here = ' '; + + $corpus_0 = <<assertEqual( + $corpus_0, + $hunks[0]->getCorpus()); + $this->assertEqual( + $corpus_1, + $hunks[1]->getCorpus()); + break; + case 'svn-ignore-whitespace-only.svndiff': + $this->assertEqual(2, count($changes)); + $hunks = reset($changes)->getHunks(); + $this->assertEqual(0, count($hunks)); + break; + case 'svn-property-add.svndiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $hunks = reset($changes)->getHunks(); + $this->assertEqual(1, count($hunks)); + $this->assertEqual( + array( + 'duck' => 'quack', + ), + $change->getNewProperties() + ); + break; + case 'svn-property-modify.svndiff': + $this->assertEqual(2, count($changes)); + + $change = array_shift($changes); + $this->assertEqual(0, count($change->getHunks())); + $this->assertEqual( + array( + 'svn:ignore' => '*.phpz', + ), + $change->getOldProperties() + ); + $this->assertEqual( + array( + 'svn:ignore' => '*.php', + ), + $change->getNewProperties() + ); + + $change = array_shift($changes); + $this->assertEqual(0, count($change->getHunks())); + $this->assertEqual( + array( + 'svn:special' => '*', + ), + $change->getOldProperties() + ); + $this->assertEqual( + array( + 'svn:special' => 'moo', + ), + $change->getNewProperties() + ); + break; + case 'svn-property-delete.svndiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + + $this->assertEqual(0, count($change->getHunks())); + $this->assertEqual( + $change->getOldProperties(), + array( + 'svn:special' => '*', + )); + $this->assertEqual( + array( + ), + $change->getNewProperties()); + break; + case 'svn-property-merged.svndiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + + $this->assertEqual(count($change->getHunks()), 0); + + $this->assertEqual( + $change->getOldProperties(), + array()); + $this->assertEqual( + $change->getNewProperties(), + array()); + break; + case 'svn-property-merge.svndiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + + $this->assertEqual(count($change->getHunks()), 0); + $this->assertEqual( + $change->getOldProperties(), + array( + )); + $this->assertEqual( + $change->getNewProperties(), + array( + 'svn:mergeinfo' => <<assertEqual(1, count($changes)); + $change = reset($changes); + $this->assertEqual( + ArcanistDiffChangeType::FILE_BINARY, + $change->getFileType()); + $this->assertEqual(0, count($change->getHunks())); + $this->assertEqual( + array( + 'svn:mime-type' => 'application/octet-stream', + ), + $change->getNewProperties() + ); + break; + case 'svn-binary-diff.svndiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $this->assertEqual( + ArcanistDiffChangeType::FILE_BINARY, + $change->getFileType()); + $this->assertEqual(count($change->getHunks()), 0); + break; + case 'git-delete-file.gitdiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $this->assertEqual( + ArcanistDiffChangeType::TYPE_DELETE, + $change->getType()); + $this->assertEqual( + 'scripts/intern/test/testfile2', + $change->getCurrentPath()); + $this->assertEqual(1, count($change->getHunks())); + break; + case 'git-binary-change.gitdiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $this->assertEqual( + ArcanistDiffChangeType::FILE_BINARY, + $change->getFileType()); + $this->assertEqual(0, count($change->getHunks())); + break; + case 'git-filemode-change.gitdiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $this->assertEqual(1, count($change->getHunks())); + $this->assertEqual( + array( + 'unix:filemode' => '100644', + ), + $change->getOldProperties() + ); + $this->assertEqual( + array( + 'unix:filemode' => '100755', + ), + $change->getNewProperties() + ); + break; + case 'git-filemode-change-only.gitdiff': + $this->assertEqual(count($changes), 2); + $change = reset($changes); + $this->assertEqual(count($change->getHunks()), 0); + $this->assertEqual( + array( + 'unix:filemode' => '100644', + ), + $change->getOldProperties() + ); + $this->assertEqual( + array( + 'unix:filemode' => '100755', + ), + $change->getNewProperties() + ); + break; + case 'svn-empty-file.svndiff': + $this->assertEqual(2, count($changes)); + $change = array_shift($changes); + $this->assertEqual(0, count($change->getHunks())); + break; + case 'git-ignore-whitespace-only.gitdiff': + $this->assertEqual(count($changes), 2); + + $change = array_shift($changes); + $this->assertEqual(count($change->getHunks()), 0); + $this->assertEqual( + $change->getOldPath(), + 'scripts/intern/test/testfile2'); + $this->assertEqual( + $change->getCurrentPath(), + 'scripts/intern/test/testfile2'); + + $change = array_shift($changes); + $this->assertEqual(count($change->getHunks()), 1); + $this->assertEqual( + $change->getOldPath(), + 'scripts/intern/test/testfile3'); + $this->assertEqual( + $change->getCurrentPath(), + 'scripts/intern/test/testfile3'); + break; + case 'git-move.gitdiff': + case 'git-move-edit.gitdiff': + case 'git-move-plus.gitdiff': + + $extra_changeset = (bool)strpos($file, '-plus'); + $has_hunk = (bool)strpos($file, '-edit'); + + $this->assertEqual($extra_changeset ? 3 : 2, count($changes)); + + $change = array_shift($changes); + $this->assertEqual($has_hunk ? 1 : 0, + count($change->getHunks())); + $this->assertEqual( + $change->getType(), + ArcanistDiffChangeType::TYPE_MOVE_HERE); + + $target = $change; + + $change = array_shift($changes); + $this->assertEqual(0, count($change->getHunks())); + $this->assertEqual( + ArcanistDiffChangeType::TYPE_MOVE_AWAY, + $change->getType() + ); + + $this->assertEqual( + $change->getCurrentPath(), + $target->getOldPath()); + $this->assertEqual( + true, + in_array($target->getCurrentPath(), $change->getAwayPaths())); + break; + case 'git-merge-header.gitdiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $this->assertEqual( + ArcanistDiffChangeType::TYPE_MESSAGE, + $change->getType()); + $this->assertEqual( + '501f6d519703458471dbea6284ec5f49d1408598', + $change->getCommitHash()); + break; + case 'git-new-file.gitdiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $this->assertEqual( + ArcanistDiffChangeType::TYPE_ADD, + $change->getType()); + break; + case 'git-copy.gitdiff': + $this->assertEqual(2, count($changes)); + + $change = array_shift($changes); + $this->assertEqual(0, count($change->getHunks())); + $this->assertEqual( + ArcanistDiffChangeType::TYPE_COPY_HERE, + $change->getType()); + $this->assertEqual( + 'flib/intern/widgets/ui/UIWidgetRSSBox.php', + $change->getCurrentPath()); + + $change = array_shift($changes); + $this->assertEqual(0, count($change->getHunks())); + $this->assertEqual( + ArcanistDiffChangeType::TYPE_COPY_AWAY, + $change->getType()); + $this->assertEqual( + 'lib/display/intern/ui/widget/UIWidgetRSSBox.php', + $change->getCurrentPath()); + + break; + case 'git-copy-plus.gitdiff': + $this->assertEqual(2, count($changes)); + + $change = array_shift($changes); + $this->assertEqual(3, count($change->getHunks())); + $this->assertEqual( + ArcanistDiffChangeType::TYPE_COPY_HERE, + $change->getType()); + $this->assertEqual( + 'flib/intern/widgets/ui/UIWidgetGraphConnect.php', + $change->getCurrentPath()); + + $change = array_shift($changes); + $this->assertEqual(0, count($change->getHunks())); + $this->assertEqual( + ArcanistDiffChangeType::TYPE_COPY_AWAY, + $change->getType()); + $this->assertEqual( + 'lib/display/intern/ui/widget/UIWidgetLunchtime.php', + $change->getCurrentPath()); + break; + case 'svn-property-multiline.svndiff': + $this->assertEqual(1, count($changes)); + $change = array_shift($changes); + + $this->assertEqual(0, count($change->getHunks())); + $this->assertEqual( + array( + 'svn:ignore' => 'tags', + ), + $change->getOldProperties() + ); + $this->assertEqual( + array( + 'svn:ignore' => "tags\nasdf\nlol\nwhat", + ), + $change->getNewProperties() + ); + break; + case 'git-commit.gitdiff': + $this->assertEqual(1, count($changes)); + $change = reset($changes); + $this->assertEqual( + ArcanistDiffChangeType::TYPE_MESSAGE, + $change->getType()); + $this->assertEqual( + '76e2f1339c298c748aa0b52030799ed202a6537b', + $change->getCommitHash()); + $this->assertEqual( + <<. I tested most + of these calls, but there were some that I didn't know how to + reach, so if you are one of the owners of this code, please test + your feature in my sandbox: www.ngao.devrs013.facebook.com + + @brosenthal, I removed some logic that was setting a disabled state + on a UIActionButton, which is actually a no-op. + +Reviewed By: brosenthal + +Other Commenters: sparker, egiovanola + +Test Plan: www.ngao.devrs013.facebook.com + + Explicitly tested: + * ads creation flow (add keyword) + * ads manager (conversion tracking) + * help center (create a discussion) + * new user wizard (next step button) + +Revert: OK + +DiffCamp Revision: 94064 + +git-svn-id: svn+ssh://tubbs/svnroot/tfb/trunk/www@223593 2c7ba8d8 +EOTEXT + , $change->getMetadata('message') + ); + break; + default: + throw new Exception("No test block for diff file {$diff_file}."); + break; + } + } +} diff --git a/src/parser/diff/__tests__/__init__.php b/src/parser/diff/__tests__/__init__.php new file mode 100644 index 00000000..70b73e47 --- /dev/null +++ b/src/parser/diff/__tests__/__init__.php @@ -0,0 +1,16 @@ + +Date: Wed Mar 3 20:39:39 2010 +0000 + + Deprecating UIActionButton (Part 1) + + Summary: Replaces calls to UIActionButton with . I tested most + of these calls, but there were some that I didn't know how to + reach, so if you are one of the owners of this code, please test + your feature in my sandbox: www.ngao.devrs013.facebook.com + + @brosenthal, I removed some logic that was setting a disabled state + on a UIActionButton, which is actually a no-op. + + Reviewed By: brosenthal + + Other Commenters: sparker, egiovanola + + Test Plan: www.ngao.devrs013.facebook.com + + Explicitly tested: + * ads creation flow (add keyword) + * ads manager (conversion tracking) + * help center (create a discussion) + * new user wizard (next step button) + + Revert: OK + + DiffCamp Revision: 94064 + + git-svn-id: svn+ssh://tubbs/svnroot/tfb/trunk/www@223593 2c7ba8d8 diff --git a/src/parser/diff/__tests__/data/git-copy-plus.gitdiff b/src/parser/diff/__tests__/data/git-copy-plus.gitdiff new file mode 100755 index 00000000..f4cba931 --- /dev/null +++ b/src/parser/diff/__tests__/data/git-copy-plus.gitdiff @@ -0,0 +1,44 @@ +diff --git a/lib/display/intern/ui/widget/UIWidgetLunchtime.php b/flib/intern/widgets/ui/UIWidgetGraphConnect.php +similarity index 57% +copy from lib/display/intern/ui/widget/UIWidgetLunchtime.php +copy to flib/intern/widgets/ui/UIWidgetGraphConnect.php +index 71268f0..ab5a8a8 100644 +--- a/lib/display/intern/ui/widget/UIWidgetLunchtime.php ++++ b/flib/intern/widgets/ui/UIWidgetGraphConnect.php +@@ -2,11 +2,12 @@ + // Copyright 2004-present Facebook. All Rights Reserved. + + /** +- * A box for lunchtime ++ * A box for connecting to the subject, could be adding a tag, adding a friend, ++ * etc... + * + * @author mnovati + */ +-class UIWidgetLunchtime extends UIWidgetBox { ++class UIWidgetGraphConnect extends UIWidgetBox { + + protected $controller = 'UIWidget'; + +@@ -17,7 +18,9 @@ class UIWidgetLunchtime extends UIWidgetBox { + */ + protected function doSetup() { + parent::doSetup(); +- $this->addClass('UIWidgetLunchtime'); ++ ++ require_static('UIWidgetGraphConnect-css'); ++ $this->addClass('UIWidgetGraphConnect'); + $this->addClass('clearfix'); + } + +@@ -28,9 +31,7 @@ class UIWidgetLunchtime extends UIWidgetBox { + * @author mnovati + */ + public function setupContent() { +- $this->setupPagelet('/intern/widget/pagelet/lunch.php', +- $context = array(), +- $is_async = true); ++ $this->setupPagelet('/intern/widget/pagelet/graph_connect.php'); + } + + } \ No newline at end of file diff --git a/src/parser/diff/__tests__/data/git-copy.gitdiff b/src/parser/diff/__tests__/data/git-copy.gitdiff new file mode 100755 index 00000000..2477de7c --- /dev/null +++ b/src/parser/diff/__tests__/data/git-copy.gitdiff @@ -0,0 +1,4 @@ +diff --git a/lib/display/intern/ui/widget/UIWidgetRSSBox.php b/flib/intern/widgets/ui/UIWidgetRSSBox.php +similarity index 100% +copy from lib/display/intern/ui/widget/UIWidgetRSSBox.php +copy to flib/intern/widgets/ui/UIWidgetRSSBox.php \ No newline at end of file diff --git a/src/parser/diff/__tests__/data/git-delete-file.gitdiff b/src/parser/diff/__tests__/data/git-delete-file.gitdiff new file mode 100755 index 00000000..82f644b6 --- /dev/null +++ b/src/parser/diff/__tests__/data/git-delete-file.gitdiff @@ -0,0 +1,19 @@ +diff --git a/scripts/intern/test/testfile2 b/scripts/intern/test/testfile2 +deleted file mode 100644 +index c654992..0000000 +--- a/scripts/intern/test/testfile2 ++++ /dev/null +@@ -1,13 +0,0 @@ +-asdfasdf +-% +-% +-%% +-%% +-%%% +-%%% +-%%%% +-%%%% +-%%%%% +-%%%%% +- +-! diff --git a/src/parser/diff/__tests__/data/git-filemode-change-only.gitdiff b/src/parser/diff/__tests__/data/git-filemode-change-only.gitdiff new file mode 100644 index 00000000..ced5d460 --- /dev/null +++ b/src/parser/diff/__tests__/data/git-filemode-change-only.gitdiff @@ -0,0 +1,14 @@ +diff --git a/last_min_rev.txt b/last_min_rev.txt +old mode 100644 +new mode 100755 +diff --git a/scripts/intern/test/testfile2 b/scripts/intern/test/testfile2 +index c654992..ef692c0 100644 +--- a/scripts/intern/test/testfile2 ++++ b/scripts/intern/test/testfile2 +@@ -9,5 +9,5 @@ asdfasdf + %%%% + %%%%% + %%%%% +- ++quack + ! diff --git a/src/parser/diff/__tests__/data/git-filemode-change.gitdiff b/src/parser/diff/__tests__/data/git-filemode-change.gitdiff new file mode 100644 index 00000000..86e961a5 --- /dev/null +++ b/src/parser/diff/__tests__/data/git-filemode-change.gitdiff @@ -0,0 +1,14 @@ +diff --git a/last_min_rev.txt b/last_min_rev.txt +old mode 100644 +new mode 100755 +index d989c70..e7c6b72 +--- a/last_min_rev.txt ++++ b/last_min_rev.txt +@@ -1,2 +1,7 @@ + 175441 + amenghra: updating minimum revision to 175441 ++a ++d ++ab ++b ++dsb diff --git a/src/parser/diff/__tests__/data/git-ignore-whitespace-only.gitdiff b/src/parser/diff/__tests__/data/git-ignore-whitespace-only.gitdiff new file mode 100755 index 00000000..4d553663 --- /dev/null +++ b/src/parser/diff/__tests__/data/git-ignore-whitespace-only.gitdiff @@ -0,0 +1,13 @@ +diff --git a/scripts/intern/test/testfile2 b/scripts/intern/test/testfile2 +index c654992..def8290 100644 +diff --git a/scripts/intern/test/testfile3 b/scripts/intern/test/testfile3 +index c654992..ef692c0 100644 +--- a/scripts/intern/test/testfile3 ++++ b/scripts/intern/test/testfile3 +@@ -9,5 +9,5 @@ asdfasdf + %%%% + %%%%% + %%%%% +- ++quack + ! diff --git a/src/parser/diff/__tests__/data/git-merge-header.gitdiff b/src/parser/diff/__tests__/data/git-merge-header.gitdiff new file mode 100755 index 00000000..f1edfe0a --- /dev/null +++ b/src/parser/diff/__tests__/data/git-merge-header.gitdiff @@ -0,0 +1,19 @@ +commit 501f6d519703458471dbea6284ec5f49d1408598 +Merge: f3d9834 e4b5d09 +Author: Hans Fugal +Date: 21 hours ago + + fbi.have_debug() to disappear + + Summary: + fbi.have_debug() is boring because libfbi has no --enable-debug-log + option - the only question is whether the debugging will do anything (if + libasox or libmc has --enable-debug-log). So it always returns true. It + will probably go away, and this is the change to go with that. + + Test Plan: + python test/test.py -d 1 + + Reviewers: marc, larry + Tags: libmc,libfbi + diff --git a/src/parser/diff/__tests__/data/git-move-edit.gitdiff b/src/parser/diff/__tests__/data/git-move-edit.gitdiff new file mode 100755 index 00000000..ad3d8613 --- /dev/null +++ b/src/parser/diff/__tests__/data/git-move-edit.gitdiff @@ -0,0 +1,16 @@ +diff --git a/html/home.php b/html/home_evan.php +similarity index 99% +rename from html/home.php +rename to html/home_evan.php +index b42cc95..fcd3819 100755 +--- a/html/home.php ++++ b/html/home_evan.php +@@ -14,7 +14,7 @@ require_module('login/context'); + + define('DEFAULT_HOME_PAGE_KEY', FEED_FILTER_KEY_DUAL_NEWS_FEED); + +-require_login(); ++// require_login(); + + $viewer_context = ViewerContext::newWebViewerContext(); + $user = $viewer_context->getUserID(); diff --git a/src/parser/diff/__tests__/data/git-move-plus.gitdiff b/src/parser/diff/__tests__/data/git-move-plus.gitdiff new file mode 100755 index 00000000..2989bf72 --- /dev/null +++ b/src/parser/diff/__tests__/data/git-move-plus.gitdiff @@ -0,0 +1,17 @@ +diff --git a/html/home.php b/html/home_evan.php +similarity index 100% +rename from html/home.php +rename to html/home_evan.php +diff --git a/scripts/intern/test/testfile3 b/scripts/intern/test/testfile3 +index c654992..614d414 100644 +--- a/scripts/intern/test/testfile3 ++++ b/scripts/intern/test/testfile3 +@@ -3,7 +3,7 @@ asdfasdf + % + %% + %% +-%%% ++%%%x + %%% + %%%% + %%%% diff --git a/src/parser/diff/__tests__/data/git-move.gitdiff b/src/parser/diff/__tests__/data/git-move.gitdiff new file mode 100755 index 00000000..758e42a7 --- /dev/null +++ b/src/parser/diff/__tests__/data/git-move.gitdiff @@ -0,0 +1,4 @@ +diff --git a/html/home.php b/html/home_evan.php +similarity index 100% +rename from html/home.php +rename to html/home_evan.php diff --git a/src/parser/diff/__tests__/data/git-new-file.gitdiff b/src/parser/diff/__tests__/data/git-new-file.gitdiff new file mode 100644 index 00000000..a3362418 --- /dev/null +++ b/src/parser/diff/__tests__/data/git-new-file.gitdiff @@ -0,0 +1,7 @@ +diff --git a/duck b/duck +new file mode 100644 +index 0000000..7984de9 +--- /dev/null ++++ b/duck +@@ -0,0 +1 @@ ++"Quack," says the duck! diff --git a/src/parser/diff/__tests__/data/svn-binary-add.svndiff b/src/parser/diff/__tests__/data/svn-binary-add.svndiff new file mode 100644 index 00000000..a7f3ebb5 --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-binary-add.svndiff @@ -0,0 +1,10 @@ +Index: binary.bin +=================================================================== +Cannot display: file marked as a binary type. +svn:mime-type = application/octet-stream + +Property changes on: binary.bin +___________________________________________________________________ +Added: svn:mime-type + + application/octet-stream + diff --git a/src/parser/diff/__tests__/data/svn-binary-diff.svndiff b/src/parser/diff/__tests__/data/svn-binary-diff.svndiff new file mode 100755 index 00000000..17723dab --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-binary-diff.svndiff @@ -0,0 +1,4 @@ +Index: html/images/sprite/autogen/8f2jjp.png +=================================================================== +Binary files html/images/sprite/autogen/8f2jjp.png (revision 223579) and html/images/sprite/autogen/8f2jjp.png (working copy) differ + diff --git a/src/parser/diff/__tests__/data/svn-empty-file.svndiff b/src/parser/diff/__tests__/data/svn-empty-file.svndiff new file mode 100644 index 00000000..5beccbf4 --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-empty-file.svndiff @@ -0,0 +1,11 @@ +Index: flib/intern/arcanist/workflow/commit/__init__.php +=================================================================== + +Property changes on: flib/intern/arcanist/workflow/commit/ArcanistCommitWorkflow.php +___________________________________________________________________ +Added: svn:mergeinfo + Merged /tfb/branches/internmove/www/lib/intern/arcanist/ArcanistCommitWorkflow.php:r83462-126155 + Merged /tfb/releases/thefacebook-r126219-10122008/www/lib/intern/arcanist/ArcanistCommitWorkflow.php:r126613,126628,126633-126634 + Merged /tfb/branches/ads-create-v3/www/lib/intern/arcanist/ArcanistCommitWorkflow.php:r140558-142418 + Merged /tfb/branches/coyote/www/lib/intern/arcanist/ArcanistCommitWorkflow.php:r167289,167325,167327,167437 + diff --git a/src/parser/diff/__tests__/data/svn-ignore-whitespace-only.svndiff b/src/parser/diff/__tests__/data/svn-ignore-whitespace-only.svndiff new file mode 100644 index 00000000..32c24259 --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-ignore-whitespace-only.svndiff @@ -0,0 +1,15 @@ +Index: scripts/intern/test/testfile2 +=================================================================== +Index: scripts/intern/test/testfile3 +=================================================================== +--- scripts/intern/test/testfile3 (revision 223313) ++++ scripts/intern/test/testfile3 (working copy) +@@ -5,7 +5,7 @@ + %% + %%% + %%% +-%%%% ++%%%%quack + %%%% + %%%%% + %%%%% diff --git a/src/parser/diff/__tests__/data/svn-property-add.svndiff b/src/parser/diff/__tests__/data/svn-property-add.svndiff new file mode 100644 index 00000000..c60de981 --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-property-add.svndiff @@ -0,0 +1,18 @@ +Index: scripts/intern/test/testfile2 +=================================================================== +--- scripts/intern/test/testfile2 (revision 223313) ++++ scripts/intern/test/testfile2 (working copy) +@@ -9,5 +9,5 @@ + %%%% + %%%%% + %%%%% +- ++quack + ! + +Property changes on: scripts/intern/test/testfile2 +___________________________________________________________________ +Added: duck + + quack + + diff --git a/src/parser/diff/__tests__/data/svn-property-delete.svndiff b/src/parser/diff/__tests__/data/svn-property-delete.svndiff new file mode 100755 index 00000000..3134b0f3 --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-property-delete.svndiff @@ -0,0 +1,6 @@ + +Property changes on: run_all_unittests +___________________________________________________________________ +Deleted: svn:special + - * + diff --git a/src/parser/diff/__tests__/data/svn-property-merge.svndiff b/src/parser/diff/__tests__/data/svn-property-merge.svndiff new file mode 100755 index 00000000..833702bb --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-property-merge.svndiff @@ -0,0 +1,5 @@ +Property changes on: html/js/help/UIFaq-Moo.js +___________________________________________________________________ +Added: svn:mergeinfo + Merged /tfb/branches/internmove/www/html/js/help/UIFaq.js:r83462-126155 + Merged /tfb/branches/ads-create-v3/www/html/js/help/UIFaq.js:r140558-142418 diff --git a/src/parser/diff/__tests__/data/svn-property-merged.svndiff b/src/parser/diff/__tests__/data/svn-property-merged.svndiff new file mode 100644 index 00000000..811235ec --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-property-merged.svndiff @@ -0,0 +1,4 @@ +Property changes on: . +___________________________________________________________________ +Modified: svn:mergeinfo + Merged /tfb/trunk/www:r276993,277571 diff --git a/src/parser/diff/__tests__/data/svn-property-modify.svndiff b/src/parser/diff/__tests__/data/svn-property-modify.svndiff new file mode 100755 index 00000000..b7f7113b --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-property-modify.svndiff @@ -0,0 +1,15 @@ +Property changes on: conf/haste +___________________________________________________________________ +Modified: svn:ignore + - *.phpz + + + *.php + + + +Property changes on: scripts/unittest/bin/run_all_unittests +___________________________________________________________________ +Modified: svn:special + - * + + moo + diff --git a/src/parser/diff/__tests__/data/svn-property-multiline.svndiff b/src/parser/diff/__tests__/data/svn-property-multiline.svndiff new file mode 100644 index 00000000..df352a1b --- /dev/null +++ b/src/parser/diff/__tests__/data/svn-property-multiline.svndiff @@ -0,0 +1,12 @@ + +Property changes on: . +___________________________________________________________________ +Modified: svn:ignore + - tags + + + tags +asdf +lol +what + + diff --git a/src/parser/diff/change/ArcanistDiffChange.php b/src/parser/diff/change/ArcanistDiffChange.php new file mode 100644 index 00000000..61a133a8 --- /dev/null +++ b/src/parser/diff/change/ArcanistDiffChange.php @@ -0,0 +1,222 @@ +hunks as $hunk) { + $hunks[] = $hunk->toDictionary(); + } + + return array( + 'metadata' => $this->metadata, + 'oldPath' => $this->oldPath, + 'currentPath' => $this->currentPath, + 'awayPaths' => $this->awayPaths, + 'oldProperties' => $this->oldProperties, + 'newProperties' => $this->newProperties, + 'type' => $this->type, + 'fileType' => $this->fileType, + 'commitHash' => $this->commitHash, + 'hunks' => $hunks, + ); + } + + public static function newFromDictionary(array $dict) { + $hunks = array(); + foreach ($dict['hunks'] as $hunk) { + $hunks[] = ArcanistDiffHunk::newFromDictionary($hunk); + } + + $obj = new ArcanistDiffChange(); + $obj->metdadata = $dict['metadata']; + $obj->oldPath = $dict['oldPath']; + $obj->currentPath = $dict['currentPath']; + $obj->awayPaths = $dict['awayPaths']; + $obj->oldProperties = $dict['oldProperties']; + $obj->newProperties = $dict['newProperties']; + $obj->type = $dict['type']; + $obj->fileType = $dict['fileType']; + $obj->commitHash = $dict['commitHash']; + $obj->hunks = $hunks; + + return $obj; + } + + public function getChangedLines($type) { + $lines = array(); + foreach ($this->hunks as $hunk) { + $lines += $hunk->getChangedLines($type); + } + return $lines; + } + + public function getAllMetadata() { + return $this->metadata; + } + + public function setMetadata($key, $value) { + $this->metadata[$key] = $value; + return $this; + } + + public function getMetadata($key) { + return idx($this->metadata, $key); + } + + public function setCommitHash($hash) { + $this->commitHash = $hash; + return $this; + } + + public function getCommitHash() { + return $this->commitHash; + } + + public function addAwayPath($path) { + $this->awayPaths[] = $path; + return $this; + } + + public function getAwayPaths() { + return $this->awayPaths; + } + + public function setFileType($type) { + $this->fileType = $type; + return $this; + } + + public function getFileType() { + return $this->fileType; + } + + public function setType($type) { + $this->type = $type; + return $this; + } + + public function getType() { + return $this->type; + } + + public function setOldProperty($key, $value) { + $this->oldProperties[$key] = $value; + return $this; + } + + public function setNewProperty($key, $value) { + $this->newProperties[$key] = $value; + return $this; + } + + public function getOldProperties() { + return $this->oldProperties; + } + + public function getNewProperties() { + return $this->newProperties; + } + + public function setCurrentPath($path) { + $this->currentPath = $this->filterPath($path); + return $this; + } + + public function getCurrentPath() { + return $this->currentPath; + } + + public function setOldPath($path) { + $this->oldPath = $this->filterPath($path); + return $this; + } + + public function getOldPath() { + return $this->oldPath; + } + + public function addHunk(ArcanistDiffHunk $hunk) { + $this->hunks[] = $hunk; + return $this; + } + + public function getHunks() { + return $this->hunks; + } + + public function convertToBinaryChange() { + $this->hunks = array(); + $this->setFileType(ArcanistDiffChangeType::FILE_BINARY); + return $this; + } + + protected function filterPath($path) { + if ($path == '/dev/null') { + return null; + } + return $path; + } + + public function renderTextSummary() { + + $type = $this->getType(); + $file = $this->getFileType(); + + $char = ArcanistDiffChangeType::getSummaryCharacterForChangeType($type); + $attr = ArcanistDiffChangeType::getShortNameForFileType($file); + if ($attr) { + $attr = '('.$attr.')'; + } + + $summary = array(); + $summary[] = sprintf( + "%s %5.5s %s", + $char, + $attr, + $this->getCurrentPath()); + if (ArcanistDiffChangeType::isOldLocationChangeType($type)) { + foreach ($this->getAwayPaths() as $path) { + $summary[] = ' to: '.$path; + } + } + if (ArcanistDiffChangeType::isNewLocationChangeType($type)) { + $summary[] = ' from: '.$this->getOldPath(); + } + + return implode("\n", $summary); + } + + +} diff --git a/src/parser/diff/change/__init__.php b/src/parser/diff/change/__init__.php new file mode 100644 index 00000000..3c75c63b --- /dev/null +++ b/src/parser/diff/change/__init__.php @@ -0,0 +1,15 @@ + 'A', + self::TYPE_CHANGE => 'M', + self::TYPE_DELETE => 'D', + self::TYPE_MOVE_AWAY => 'V', + self::TYPE_COPY_AWAY => 'P', + self::TYPE_MOVE_HERE => 'V', + self::TYPE_COPY_HERE => 'P', + self::TYPE_MULTICOPY => 'P', + self::TYPE_MESSAGE => 'Q', + self::TYPE_CHILD => '@', + ); + return idx($types, coalesce($type, '?'), '~'); + } + + public static function getShortNameForFileType($type) { + static $names = array( + self::FILE_TEXT => null, + self::FILE_DIRECTORY => 'dir', + self::FILE_IMAGE => 'img', + self::FILE_BINARY => 'bin', + self::FILE_SYMLINK => 'sym', + ); + return idx($names, coalesce($type, '?'), '???'); + } + + public static function isOldLocationChangeType($type) { + static $types = array( + ArcanistDiffChangeType::TYPE_MOVE_AWAY => true, + ArcanistDiffChangeType::TYPE_COPY_AWAY => true, + ArcanistDiffChangeType::TYPE_MULTICOPY => true, + ); + return isset($types[$type]); + } + + public static function isNewLocationChangeType($type) { + static $types = array( + ArcanistDiffChangeType::TYPE_MOVE_HERE => true, + ArcanistDiffChangeType::TYPE_COPY_HERE => true, + ); + return isset($types[$type]); + } + + public static function isDeleteChangeType($type) { + static $types = array( + ArcanistDiffChangeType::TYPE_DELETE => true, + ArcanistDiffChangeType::TYPE_MOVE_AWAY => true, + ArcanistDiffChangeType::TYPE_MULTICOPY => true, + ); + return isset($types[$type]); + } + + public static function isCreateChangeType($type) { + static $types = array( + ArcanistDiffChangeType::TYPE_ADD => true, + ArcanistDiffChangeType::TYPE_COPY_HERE => true, + ArcanistDiffChangeType::TYPE_MOVE_HERE => true, + ); + return isset($types[$type]); + } + + public static function isModifyChangeType($type) { + static $types = array( + ArcanistDiffChangeType::TYPE_CHANGE => true, + ); + return isset($types[$type]); + } + + public static function getFullNameForChangeType($type) { + static $types = array( + self::TYPE_ADD => 'Added', + self::TYPE_CHANGE => 'Modified', + self::TYPE_DELETE => 'Deleted', + self::TYPE_MOVE_AWAY => 'Moved Away', + self::TYPE_COPY_AWAY => 'Copied Away', + self::TYPE_MOVE_HERE => 'Moved Here', + self::TYPE_COPY_HERE => 'Copied Here', + self::TYPE_MULTICOPY => 'Deleted After Multiple Copy', + self::TYPE_MESSAGE => 'Commit Message', + self::TYPE_CHILD => 'Contents Modified', + ); + return idx($types, coalesce($type, '?'), 'Unknown'); + } + +} diff --git a/src/parser/diff/changetype/__init__.php b/src/parser/diff/changetype/__init__.php new file mode 100644 index 00000000..0a32ccee --- /dev/null +++ b/src/parser/diff/changetype/__init__.php @@ -0,0 +1,21 @@ + $this->oldOffset, + 'newOffset' => $this->newOffset, + 'oldLength' => $this->oldLength, + 'newLength' => $this->newLength, + 'addLines' => $this->addLines, + 'delLines' => $this->delLines, + 'isMissingOldNewline' => $this->isMissingOldNewline, + 'isMissingNewNewline' => $this->isMissingNewNewline, + 'corpus' => $this->corpus, + ); + } + + public function newFromDictionary(array $dict) { + $obj = new ArcanistDiffHunk(); + + $obj->oldOffset = $dict['oldOffset']; + $obj->newOffset = $dict['newOffset']; + $obj->oldLength = $dict['oldLength']; + $obj->newLength = $dict['newLength']; + $obj->addLines = $dict['addLines']; + $obj->delLines = $dict['delLines']; + $obj->isMissingOldNewline = $dict['isMissingOldNewline']; + $obj->isMissingNewNewline = $dict['isMissingNewNewline']; + $obj->corpus = $dict['corpus']; + + return $obj; + } + + public function getChangedLines($type) { + $old_map = array(); + $new_map = array(); + $cover_map = array(); + + $oline = $this->getOldOffset(); + $nline = $this->getNewOffset(); + foreach (explode("\n", $this->getCorpus()) as $line) { + $char = strlen($line) ? $line[0] : '~'; + switch ($char) { + case '-': + $old_map[$oline] = true; + $cover_map[$oline] = true; + ++$oline; + break; + case '+': + $new_map[$nline] = true; + if ($oline > 1) { + $cover_map[$oline - 1] = true; + } + $cover_map[$oline] = true; + ++$nline; + break; + default: + ++$oline; + ++$nline; + break; + } + } + + switch ($type) { + case 'new': + return $new_map; + case 'old': + return $old_map; + case 'cover': + return $cover_map; + default: + throw new Exception("Unknown line change type '{$type}'."); + } + } + + public function setOldOffset($old_offset) { + $this->oldOffset = $old_offset; + return $this; + } + + public function getOldOffset() { + return $this->oldOffset; + } + + public function setNewOffset($new_offset) { + $this->newOffset = $new_offset; + return $this; + } + + public function getNewOffset() { + return $this->newOffset; + } + + public function setOldLength($old_length) { + $this->oldLength = $old_length; + return $this; + } + + public function getOldLength() { + return $this->oldLength; + } + + public function setNewLength($new_length) { + $this->newLength = $new_length; + return $this; + } + + public function getNewLength() { + return $this->newLength; + } + + public function setAddLines($add_lines) { + $this->addLines = $add_lines; + return $this; + } + + public function getAddLines() { + return $this->addLines; + } + + public function setDelLines($del_lines) { + $this->delLines = $del_lines; + return $this; + } + + public function getDelLines() { + return $this->delLines; + } + + public function setCorpus($corpus) { + $this->corpus = $corpus; + return $this; + } + + public function getCorpus() { + return $this->corpus; + } + + public function setIsMissingOldNewline($missing) { + $this->isMissingOldNewline = (bool)$missing; + return $this; + } + + public function getIsMissingOldNewline() { + return $this->isMissingOldNewline; + } + + public function setIsMissingNewNewline($missing) { + $this->isMissingNewNewline = (bool)$missing; + return $this; + } + + public function getIsMissingNewNewline() { + return $this->isMissingNewNewline; + } + +} diff --git a/src/parser/diff/hunk/__init__.php b/src/parser/diff/hunk/__init__.php new file mode 100644 index 00000000..2318f72a --- /dev/null +++ b/src/parser/diff/hunk/__init__.php @@ -0,0 +1,19 @@ +diffLinesOfContext; + } + + public function setDiffLinesOfContext($lines) { + $this->diffLinesOfContext = $lines; + return $this; + } + + public static function newAPIFromWorkingCopyIdentity( + ArcanistWorkingCopyIdentity $working_copy) { + + $root = $working_copy->getProjectRoot(); + + if (!$root) { + throw new ArcanistUsageException( + "There is no readable '.arcconfig' file in the working directory or ". + "any parent directory. Create an '.arcconfig' file to configure arc."); + } + + if (@file_exists($root.'/.svn')) { + phutil_require_module('arcanist', 'repository/api/subversion'); + return new ArcanistSubversionAPI($root); + } + + $git_root = self::discoverGitBaseDirectory($root); + if ($git_root) { + if (!Filesystem::pathsAreEquivalent($root, $git_root)) { + throw new ArcanistUsageException( + "'.arcconfig' file is located at '{$root}', but working copy root ". + "is '{$git_root}'. Move '.arcconfig' file to the working copy root."); + } + phutil_require_module('arcanist', 'repository/api/git'); + return new ArcanistGitAPI($root); + } + + throw new ArcanistUsageException( + "The current working directory is not part of a working copy for a ". + "supported version control system (svn or git)."); + } + + protected function __construct($path) { + $this->path = $path; + } + + public function getPath($to_file = null) { + if ($to_file !== null) { + return $this->path.'/'.ltrim($to_file, '/'); + } else { + return $this->path.'/'; + } + } + + public function getUntrackedChanges() { + return $this->getWorkingCopyFilesWithMask(self::FLAG_UNTRACKED); + } + + public function getUnstagedChanges() { + return $this->getWorkingCopyFilesWithMask(self::FLAG_UNSTAGED); + } + + public function getUncommittedChanges() { + return $this->getWorkingCopyFilesWithMask(self::FLAG_UNCOMMITTED); + } + + public function getMergeConflicts() { + return $this->getWorkingCopyFilesWithMask(self::FLAG_CONFLICT); + } + + private function getWorkingCopyFilesWithMask($mask) { + $match = array(); + foreach ($this->getWorkingCopyStatus() as $file => $flags) { + if ($flags & $mask) { + $match[] = $file; + } + } + return $match; + } + + private static function discoverGitBaseDirectory($root) { + try { + list($stdout) = execx( + '(cd %s; git rev-parse --show-cdup)', + $root); + return Filesystem::resolvePath(rtrim($stdout, "\n"), $root); + } catch (CommandException $ex) { + if (preg_match('/^fatal: Not a git repository/', $ex->getStdErr())) { + return null; + } + throw $ex; + } + } + + abstract public function getBlame($path); + abstract public function getWorkingCopyStatus(); + abstract public function getRawDiffText($path); + +} diff --git a/src/repository/api/base/__init__.php b/src/repository/api/base/__init__.php new file mode 100644 index 00000000..ad7f82cd --- /dev/null +++ b/src/repository/api/base/__init__.php @@ -0,0 +1,15 @@ +relativeCommit = $relative_commit; + return $this; + } + + public function getRelativeCommit() { + if ($this->relativeCommit === null) { + list($err) = exec_manual( + '(cd %s; git rev-parse --verify HEAD^)', + $this->getPath()); + if ($err) { + $this->relativeCommit = self::GIT_MAGIC_ROOT_COMMIT; + } else { + $this->relativeCommit = 'HEAD^'; + } + } + return $this->relativeCommit; + } + + private function getDiffOptions() { + $options = array( + '-M', + '-C', + '--no-ext-diff', + '--no-color', + '--src-prefix=a/', + '--dst-prefix=b/', + '-U'.$this->getDiffLinesOfContext(), + ); + return implode(' ', $options); + } + + public function getFullGitDiff() { + $options = $this->getDiffOptions(); + list($stdout) = execx( + "(cd %s; git diff {$options} %s --)", + $this->getPath(), + $this->getRelativeCommit()); + return $stdout; + } + + public function getRawDiffText($path) { + $relative_commit = $this->getRelativeCommit(); + $options = $this->getDiffOptions(); + list($stdout) = execx( + "(cd %s; git diff {$options} %s -- %s)", + $this->getPath(), + $this->getRelativeCommit(), + $path); + return $stdout; + } + + public function getBranchName() { + // TODO: consider: + // + // $ git rev-parse --abbrev-ref `git symbolic-ref HEAD` + // + // But that may fail if you're not on a branch. + list($stdout) = execx( + '(cd %s; git branch)', + $this->getPath()); + + $matches = null; + if (preg_match('/^\* (.+)$/m', $stdout, $matches)) { + return $matches[1]; + } + return null; + } + + public function getSourceControlPath() { + // TODO: Try to get something useful here. + return null; + } + + public function getGitCommitLog() { + $relative = $this->getRelativeCommit(); + if ($relative == self::GIT_MAGIC_ROOT_COMMIT) { + list($stdout) = execx( + '(cd %s; git log HEAD)', + $this->getPath()); + } else { + list($stdout) = execx( + '(cd %s; git log %s..HEAD)', + $this->getPath(), + $this->getRelativeCommit()); + } + return $stdout; + } + + public function getGitHistoryLog() { + list($stdout) = execx( + '(cd %s; git log -n%d %s)', + $this->getPath(), + self::SEARCH_LENGTH_FOR_PARENT_REVISIONS, + $this->getRelativeCommit()); + return $stdout; + } + + public function getSourceControlBaseRevision() { + list($stdout) = execx( + '(cd %s; git rev-parse %s)', + $this->getPath(), + $this->getRelativeCommit()); + return rtrim($stdout, "\n"); + } + + public function getGitHeadRevision() { + list($stdout) = execx( + '(cd %s; git rev-parse HEAD)', + $this->getPath()); + return rtrim($stdout, "\n"); + } + + public function getWorkingCopyStatus() { + if (!isset($this->status)) { + + // Find committed changes. + list($stdout) = execx( + '(cd %s; git diff --no-ext-diff --raw %s --)', + $this->getPath(), + $this->getRelativeCommit()); + $files = $this->parseGitStatus($stdout); + + // Find uncommitted changes. + list($stdout) = execx( + '(cd %s; git diff --no-ext-diff --raw HEAD --)', + $this->getPath()); + $files += $this->parseGitStatus($stdout); + + // Find untracked files. + list($stdout) = execx( + '(cd %s; git ls-files --others --exclude-standard)', + $this->getPath()); + $stdout = rtrim($stdout, "\n"); + if (strlen($stdout)) { + $stdout = explode("\n", $stdout); + foreach ($stdout as $file) { + $files[$file] = self::FLAG_UNTRACKED; + } + } + + // Find unstaged changes. + list($stdout) = execx( + '(cd %s; git ls-files -m)', + $this->getPath()); + $stdout = rtrim($stdout, "\n"); + if (strlen($stdout)) { + $stdout = explode("\n", $stdout); + foreach ($stdout as $file) { + $files[$file] = self::FLAG_UNSTAGED; + } + } + + $this->status = $files; + } + + return $this->status; + } + + public function amendGitHeadCommit($message) { + execx( + '(cd %s; git commit --amend --message %s)', + $this->getPath(), + $message); + } + + public function getPreReceiveHookStatus($old_ref, $new_ref) { + list($stdout) = execx( + '(cd %s && git diff --no-ext-diff --raw %s %s --)', + $this->getPath(), + $old_ref, + $new_ref); + return $this->parseGitStatus($stdout, $full = true); + } + + private function parseGitStatus($status, $full = false) { + static $flags = array( + 'A' => self::FLAG_ADDED, + 'M' => self::FLAG_MODIFIED, + 'D' => self::FLAG_DELETED, + ); + + $status = trim($status); + $lines = array(); + foreach (explode("\n", $status) as $line) { + if ($line) { + $lines[] = preg_split("/[ \t]/", $line); + } + } + + $files = array(); + foreach ($lines as $line) { + $mask = 0; + $flag = $line[4]; + $file = $line[5]; + foreach ($flags as $key => $bits) { + if ($flag == $key) { + $mask |= $bits; + } + } + if ($full) { + $files[$file] = array( + 'mask' => $mask, + 'ref' => rtrim($line[3], '.'), + ); + } else { + $files[$file] = $mask; + } + } + + return $files; + } + + public function getBlame($path) { + // TODO: 'git blame' supports --porcelain and we should probably use it. + list($stdout) = execx( + '(cd %s; git blame -w -C %s -- %s)', + $this->getPath(), + $this->getRelativeCommit(), + $path); + + $blame = array(); + foreach (explode("\n", trim($stdout)) as $line) { + if (!strlen($line)) { + continue; + } + + // lines predating a git repo's history are blamed to the oldest revision, + // with the commit hash prepended by a ^. we shouldn't count these lines + // as blaming to the oldest diff's unfortunate author + if ($line[0] == '^') { + continue; + } + + $matches = null; + $ok = preg_match( + '/^([0-9a-f]+)[^(]+?[(](.*?) +\d\d\d\d-\d\d-\d\d/', + $line, + $matches); + if (!$ok) { + throw new Exception("Bad blame? `{$line}'"); + } + $revision = $matches[1]; + $author = $matches[2]; + + $blame[] = array($author, $revision); + } + + return $blame; + } + +} diff --git a/src/repository/api/git/__init__.php b/src/repository/api/git/__init__.php new file mode 100644 index 00000000..0ee9f3f6 --- /dev/null +++ b/src/repository/api/git/__init__.php @@ -0,0 +1,14 @@ +getSVNStatus() as $path => $mask) { + if ($mask & self::FLAG_CONFLICT) { + return true; + } + } + return false; + } + + public function getWorkingCopyStatus() { + return $this->getSVNStatus(); + } + + public function getSVNBaseRevisions() { + if ($this->svnBaseRevisions === null) { + $this->getSVNStatus(); + } + return $this->svnBaseRevisions; + } + + public function getSVNStatus($with_externals = false) { + if ($this->svnStatus === null) { + list($status) = execx('(cd %s && svn --xml status)', $this->getPath()); + $xml = new SimpleXMLElement($status); + + if (count($xml->target) != 1) { + throw new Exception("Expected exactly one XML status target."); + } + + $externals = array(); + $files = array(); + + $target = $xml->target[0]; + $this->svnBaseRevisions = array(); + foreach ($target->entry as $entry) { + $path = (string)$entry['path']; + $mask = 0; + + $props = (string)($entry->{'wc-status'}[0]['props']); + $item = (string)($entry->{'wc-status'}[0]['item']); + + $base = (string)($entry->{'wc-status'}[0]['revision']); + $this->svnBaseRevisions[$path] = $base; + + switch ($props) { + case 'none': + case 'normal': + break; + case 'modified': + $mask |= self::FLAG_MODIFIED; + break; + default: + throw new Exception("Unrecognized property status '{$props}'."); + } + + switch ($item) { + case 'normal': + break; + case 'external': + $mask |= self::FLAG_EXTERNALS; + $externals[] = $path; + break; + case 'unversioned': + $mask |= self::FLAG_UNTRACKED; + break; + case 'obstructed': + $mask |= self::FLAG_OBSTRUCTED; + break; + case 'missing': + $mask |= self::FLAG_MISSING; + break; + case 'added': + $mask |= self::FLAG_ADDED; + break; + case 'modified': + $mask |= self::FLAG_MODIFIED; + break; + case 'deleted': + $mask |= self::FLAG_DELETED; + break; + default: + throw new Exception("Unrecognized item status '{$item}'."); + } + + $files[$path] = $mask; + } + + foreach ($files as $path => $mask) { + foreach ($externals as $external) { + if (!strncmp($path, $external, strlen($external))) { + $files[$path] |= self::FLAG_EXTERNALS; + } + } + } + + $this->svnStatus = $files; + } + + $status = $this->svnStatus; + if (!$with_externals) { + foreach ($status as $path => $mask) { + if ($mask & ArcanistRepositoryAPI::FLAG_EXTERNALS) { + unset($status[$path]); + } + } + } + + return $status; + } + + public function getSVNProperty($path, $property) { + list($stdout) = execx( + 'svn propget %s %s@', + $property, + $this->getPath($path)); + return trim($stdout); + } + + public function getSourceControlPath() { + return idx($this->getSVNInfo('/'), 'URL'); + } + + public function getSourceControlBaseRevision() { + $info = $this->getSVNInfo('/'); + return $info['URL'].'@'.$info['Revision']; + } + + public function getBranchName() { + return 'svn'; + } + + public function buildInfoFuture($path) { + // Note: here and elsewhere we need to append "@" to the path because if + // a file has a literal "@" in it, everything after that will be + // interpreted as a revision. By appending "@" with no argument, SVN + // parses it properly. + return new ExecFuture( + 'svn info %s@', + $this->getPath($path)); + } + + public function buildDiffFuture($path) { + // The "--depth empty" flag prevents us from picking up changes in + // children when we run 'diff' against a directory. + return new ExecFuture( + '(cd %s; svn diff --depth empty --diff-cmd diff -x -U%d %s)', + $this->getPath(), + $this->getDiffLinesOfContext(), + $path); + } + + public function primeSVNInfoResult($path, $result) { + $this->svnInfoRaw[$path] = $result; + return $this; + } + + public function primeSVNDiffResult($path, $result) { + $this->svnDiffRaw[$path] = $result; + return $this; + } + + public function getSVNInfo($path) { + + if (empty($this->svnInfo[$path])) { + + if (empty($this->svnInfoRaw[$path])) { + $this->svnInfoRaw[$path] = $this->buildInfoFuture($path)->resolve(); + } + + list($err, $stdout) = $this->svnInfoRaw[$path]; + if ($err) { + throw new Exception( + "Error #{$err} executing svn info against '{$path}'."); + } + + $patterns = array( + '/^(URL): (\S+)$/m', + '/^(Revision): (\d+)$/m', + '/^(Last Changed Author): (\S+)$/m', + '/^(Last Changed Rev): (\d+)$/m', + '/^(Last Changed Date): (.+) \(.+\)$/m', + '/^(Copied From URL): (\S+)$/m', + '/^(Copied From Rev): (\d+)$/m', + ); + + $result = array(); + foreach ($patterns as $pattern) { + $matches = null; + if (preg_match($pattern, $stdout, $matches)) { + $result[$matches[1]] = $matches[2]; + } + } + + if (isset($result['Last Changed Date'])) { + $result['Last Changed Date'] = strtotime($result['Last Changed Date']); + } + + if (empty($result)) { + throw new Exception('Unable to parse SVN info.'); + } + + $this->svnInfo[$path] = $result; + } + + return $this->svnInfo[$path]; + } + + + public function getRawDiffText($path) { + $status = $this->getSVNStatus(); + if (!isset($status[$path])) { + return null; + } + + $status = $status[$path]; + + // Build meaningful diff text for "svn copy" operations. + if ($status & ArcanistRepositoryAPI::FLAG_ADDED) { + $info = $this->getSVNInfo($path); + if (!empty($info['Copied From URL'])) { + return $this->buildSyntheticAdditionDiff( + $path, + $info['Copied From URL'], + $info['Copied From Rev']); + } + } + + // If we run "diff" on a binary file which doesn't have the "svn:mime-type" + // of "application/octet-stream", `diff' will explode in a rain of + // unhelpful hellfire as it tries to build a textual diff of the two + // files. We just fix this inline since it's pretty unambiguous. + // TODO: Move this to configuration? + $matches = null; + if (preg_match('/\.(gif|png|jpe?g|swf|pdf|ico)$/i', $path, $matches)) { + $mime = $this->getSVNProperty($path, 'svn:mime-type'); + if ($mime != 'application/octet-stream') { + execx( + 'svn propset svn:mime-type application/octet-stream %s', + $this->getPath($path)); + } + } + + if (empty($this->svnDiffRaw[$path])) { + $this->svnDiffRaw[$path] = $this->buildDiffFuture($path)->resolve(); + } + + list($err, $stdout, $stderr) = $this->svnDiffRaw[$path]; + + // Note: GNU Diff returns 2 when SVN hands it binary files to diff and they + // differ. This is not an error; it is documented behavior. But SVN isn't + // happy about it. SVN will exit with code 1 and return the string below. + if ($err != 0 && $stderr !== "svn: 'diff' returned 2\n") { + throw new Exception( + "svn diff returned unexpected error code: $err\n". + "stdout: $stdout\n". + "stderr: $stderr"); + } + + if ($err == 0 && empty($stdout)) { + // If there are no changes, 'diff' exits with no output, but that means + // we can not distinguish between empty and unmodified files. Build a + // synthetic "diff" without any changes in it. + return $this->buildSyntheticUnchangedDiff($path); + } + + return $stdout; + } + + protected function buildSyntheticAdditionDiff($path, $source, $rev) { + $type = $this->getSVNProperty($path, 'svn:mime-type'); + if ($type == 'application/octet-stream') { + return <<getPath($path))) { + return null; + } + + $data = Filesystem::readFile($this->getPath($path)); + list($orig) = execx('svn cat %s@%s', $source, $rev); + + $src = new TempFile(); + $dst = new TempFile(); + Filesystem::writeFile($src, $orig); + Filesystem::writeFile($dst, $data); + + list($err, $diff) = exec_manual( + 'diff -L a/%s -L b/%s -U%d %s %s', + str_replace($this->getSourceControlPath().'/', '', $source), + $path, + $this->getDiffLinesOfContext(), + $src, + $dst); + + if ($err == 1) { // 1 means there are differences. + return <<buildSyntheticUnchangedDiff($path); + } + } + + protected function buildSyntheticUnchangedDiff($path) { + $full_path = $this->getPath($path); + if (is_dir($full_path)) { + return null; + } + + $data = Filesystem::readFile($full_path); + $lines = explode("\n", $data); + $len = count($lines); + foreach ($lines as $key => $line) { + $lines[$key] = ' '.$line; + } + $lines = implode("\n", $lines); + return <<getPath(), + $path); + + $stdout = trim($stdout); + if (!strlen($stdout)) { + // Empty file. + return $blame; + } + + foreach (explode("\n", $stdout) as $line) { + $m = array(); + if (!preg_match('/^\s*(\d+)\s+(\S+)/', $line, $m)) { + throw new Exception("Bad blame? `{$line}'"); + } + $revision = $m[1]; + $author = $m[2]; + $blame[] = array($author, $revision); + } + + return $blame; + } + +} diff --git a/src/repository/api/subversion/__init__.php b/src/repository/api/subversion/__init__.php new file mode 100644 index 00000000..d65736bf --- /dev/null +++ b/src/repository/api/subversion/__init__.php @@ -0,0 +1,17 @@ + array(), + 'interface' => array(), + 'function' => array(), + ); + + protected $requires = array( + 'class' => array(), + 'interface' => array(), + 'function' => array(), + 'source' => array(), + 'module' => array(), + ); + + protected $declares = array( + 'class' => array(), + 'interface' => array(), + 'function' => array(), + 'source' => array(), + ); + + protected $chain = array( + ); + + protected $currentFile; + protected $messages = array( + ); + + public function setCurrentFile($current_file) { + $this->currentFile = $current_file; + return $this; + } + + protected function getCurrentFile() { + return $this->currentFile; + } + + protected function getWhere(XHPASTNode $where) { + return $this->getCurrentFile().':'.$where->getOffset(); + } + + public function addClassDeclaration(XHPASTNode $where, $name) { + return $this->addDeclaration('class', $where, $name); + } + + public function addFunctionDeclaration(XHPASTNode $where, $name) { + return $this->addDeclaration('function', $where, $name); + } + + public function addInterfaceDeclaration(XHPASTNode $where, $name) { + return $this->addDeclaration('interface', $where, $name); + } + + public function addSourceDeclaration($name) { + $this->declares['source'][$name] = true; + return $this; + } + + protected function addDeclaration($type, XHPASTNode $where, $name) { + $this->declares[$type][$name] = $this->getWhere($where); + return $this; + } + + protected function addDependency($type, XHPASTNode $where, $name) { + if (isset($this->builtins[$type][$name])) { + return $this; + } + if (empty($this->requires[$type][$name])) { + $this->requires[$type][$name] = array(); + } + $this->requires[$type][$name][] = $this->getWhere($where); + return $this; + } + + public function addClassDependency($child, XHPASTNode $where, $name) { + if ($child !== null) { + if (empty($this->builtins['class'][$name])) { + $this->chain['class'][$child] = $name; + } + } + return $this->addDependency('class', $where, $name); + } + + public function addFunctionDependency(XHPASTNode $where, $name) { + return $this->addDependency('function', $where, $name); + } + + public function addInterfaceDependency($child, XHPASTNode $where, $name) { + if ($child !== null) { + if (empty($this->builtins['interface'][$name])) { + $this->chain['interface'][$child][] = $name; + } + } + return $this->addDependency('interface', $where, $name); + } + + public function addSourceDependency(XHPASTNode $where, $name) { + return $this->addDependency('source', $where, $name); + } + + public function addModuleDependency(XHPASTNode $where, $name) { + return $this->addDependency('module', $where, $name); + } + + public function addBuiltins(array $builtins) { + foreach ($builtins as $type => $symbol_set) { + $this->builtins[$type] += $symbol_set; + } + return $this; + } + + public function addRawLint($code, $message) { + $this->messages[] = array( + null, + null, + $code, + $message); + return $this; + } + + public function addLint(XHPASTNode $where, $text, $code, $message) { + $this->messages[] = array( + $this->getWhere($where), + $text, + $code, + $message); + return $this; + } + + public function toDictionary() { + + // Remove all dependencies on things which we declare since they're never + // useful and guaranteed to be satisfied. + foreach ($this->declares as $type => $things) { + if ($type == 'source') { + // Source is treated specially since we only reconcile it locally. + continue; + } + foreach ($things as $name => $where) { + unset($this->requires[$type][$name]); + } + } + + return array( + 'declares' => $this->declares, + 'requires' => $this->requires, + 'chain' => $this->chain, + 'messages' => $this->messages, + ); + } +} diff --git a/src/staticanalysis/parsers/phutilmodule/__init__.php b/src/staticanalysis/parsers/phutilmodule/__init__.php new file mode 100644 index 00000000..c4a18cb0 --- /dev/null +++ b/src/staticanalysis/parsers/phutilmodule/__init__.php @@ -0,0 +1,19 @@ +errorLine = $line; + parent::__construct($message); + } + + public function getErrorLine() { + return $this->errorLine; + } + +} diff --git a/src/staticanalysis/parsers/xhpast/api/exception/__init__.php b/src/staticanalysis/parsers/xhpast/api/exception/__init__.php new file mode 100644 index 00000000..8f31d953 --- /dev/null +++ b/src/staticanalysis/parsers/xhpast/api/exception/__init__.php @@ -0,0 +1,10 @@ +ids); + } + + public function current() { + return $this->list[$this->key()]; + } + + public function rewind() { + $this->pos = 0; + } + + public function valid() { + return $this->pos < count($this->ids); + } + + public function next() { + $this->pos++; + } + + public function key() { + return $this->ids[$this->pos]; + } + + public static function newFromTreeAndNodes(XHPASTTree $tree, array $nodes) { + $obj = new XHPASTNodeList(); + $obj->tree = $tree; + $obj->list = $nodes; + $obj->ids = array_keys($nodes); + return $obj; + } + + public static function newFromTree(XHPASTTree $tree) { + $obj = new XHPASTNodeList(); + $obj->tree = $tree; + $obj->list = array(0 => $tree->getRootNode()); + $obj->ids = array(0 => 0); + return $obj; + } + + protected function __construct() { + + } + + public function getDescription() { + if (empty($this->list)) { + return 'an empty node list'; + } + + $desc = array(); + $desc[] = "a list of ".count($this->list)." nodes:"; + foreach ($this->list as $node) { + $desc[] = ' '.$node->getDescription().";"; + } + + return implode("\n", $desc); + } + + + + protected function newList(array $nodes) { + return XHPASTNodeList::newFromTreeAndNodes( + $this->tree, + $nodes); + } + + public function selectDescendantsOfType($type_name) { + $results = array(); + foreach ($this->list as $id => $node) { + $results += $node->selectDescendantsOfType($type_name)->getRawNodes(); + } + return $this->newList($results); + } + + public function selectDescendantsOfTypes(array $type_names) { + $results = array(); + foreach ($type_names as $type_name) { + foreach ($this->list as $id => $node) { + $results += $node->selectDescendantsOfType($type_name)->getRawNodes(); + } + } + return $this->newList($results); + } + + public function getChildrenByIndex($index) { + $results = array(); + foreach ($this->list as $id => $node) { + $child = $node->getChildByIndex($index); + $results[$child->getID()] = $child; + } + return $this->newList($results); + } + + public function add(XHPASTNodeList $list) { + foreach ($list->list as $id => $node) { + $this->list[$id] = $node; + } + $this->ids = array_keys($this->list); + return $this; + } + + + protected function executeSelectDescendantsOfType($node, $type) { + $results = array(); + foreach ($node->getChildren() as $id => $child) { + if ($child->getTypeID() == $type) { + $results[$id] = $child; + } else { + $results += $this->executeSelectDescendantsOfType($child, $type); + } + } + return $results; + } + + public function getTokens() { + $tokens = array(); + foreach ($this->list as $node) { + $tokens += $node->getTokens(); + } + return $tokens; + } + + public function getRawNodes() { + return $this->list; + } + +} diff --git a/src/staticanalysis/parsers/xhpast/api/list/__init__.php b/src/staticanalysis/parsers/xhpast/api/list/__init__.php new file mode 100644 index 00000000..6c192345 --- /dev/null +++ b/src/staticanalysis/parsers/xhpast/api/list/__init__.php @@ -0,0 +1,19 @@ +id = $id; + $this->typeID = $data[0]; + $this->l = idx($data, 1, -1); + $this->r = idx($data, 2, -1); + $this->tree = $tree; + } + + public function setParentNode($parent_node) { + $this->parentNode = $parent_node; + return $this; + } + + public function getParentNode() { + return $this->parentNode; + } + + public function setChildren(array $children) { + $this->children = $children; + return $this; + } + + public function getID() { + return $this->id; + } + + public function getTypeID() { + return $this->typeID; + } + + public function getTypeName() { + static $map; + if (empty($map)) { + $map = xhp_parser_node_constants(); + } + + $type_id = $this->getTypeID(); + if (empty($map[$type_id])) { + throw new Exception("No type name for node type ID '{$type_id}'."); + } + + return $map[$type_id]; + } + + public function getChildren() { + return $this->children; + } + + public function getChildOfType($index, $type) { + $child = $this->getChildByIndex($index); + if ($child->getTypeName() != $type) { + throw new Exception( + "Child in position '{$index}' is not of type '{$type}': ". + $this->getDescription()); + } + + return $child; + } + + public function getChildByIndex($index) { + $child = idx(array_values($this->children), $index); + if (!$child) { + throw new Exception( + "No child with index '{$index}'."); + } + return $child; + } + + public function selectDescendantsOfType($type_name) { + $type = $this->getTypeIDFromTypeName($type_name); + return XHPASTNodeList::newFromTreeAndNodes( + $this->tree, + $this->executeSelectDescendantsOfType($this, $type)); + } + + protected function executeSelectDescendantsOfType($node, $type) { + $results = array(); + foreach ($node->getChildren() as $id => $child) { + if ($child->getTypeID() == $type) { + $results[$id] = $child; + } + $results += $this->executeSelectDescendantsOfType($child, $type); + } + return $results; + } + + public function getTokens() { + if ($this->l == -1 || $this->r == -1) { + return array(); + } + $tokens = $this->tree->getRawTokenStream(); + $result = array(); + foreach (range($this->l, $this->r) as $token_id) { + $result[$token_id] = $tokens[$token_id]; + } + return $result; + } + + public function getConcreteString() { + $values = array(); + foreach ($this->getTokens() as $token) { + $values[] = $token->getValue(); + } + return implode('', $values); + } + + public function getStringLiteralValue() { + // TODO: This function should accommodate concatenation of literals and + // return 'null' if the literal contains variables. + + if ($this->getTypeName() != 'n_STRING_SCALAR') { + return null; + } + $value = $this->getConcreteString(); + $value = substr($value, 1, -1); + $value = stripcslashes($value); + return $value; + } + + public function getSemanticString() { + $tokens = $this->getTokens(); + foreach ($tokens as $id => $token) { + if ($token->isComment()) { + unset($tokens[$id]); + } + } + return implode('', mpull($tokens, 'getValue')); + } + + public function getDescription() { + $concrete = $this->getConcreteString(); + if (strlen($concrete) > 75) { + $concrete = substr($concrete, 0, 36).'...'.substr($concrete, -36); + } + + $concrete = addcslashes($concrete, "\\\n\""); + + return 'a node of type '.$this->getTypeName().': "'.$concrete.'"'; + } + + protected function getTypeIDFromTypeName($type_name) { + static $node_types; + if (empty($node_types)) { + $node_types = xhp_parser_node_constants(); + $node_types = array_flip($node_types); + } + + if (empty($node_types[$type_name])) { + throw new Exception("Unknown XHPAST Node type name '{$type_name}'!"); + } + + return $node_types[$type_name]; + } + + public function getOffset() { + $first_token = idx($this->tree->getRawTokenStream(), $this->l); + if (!$first_token) { + return null; + } + return $first_token->getOffset(); + } + + public function isStaticScalar() { + return ($this->getTypeName() == 'n_STRING_SCALAR' || + $this->getTypeName() == 'n_NUMERIC_SCALAR'); + } + + public function getSurroundingNonsemanticTokens() { + $before = array(); + $after = array(); + + $tokens = $this->tree->getRawTokenStream(); + + if ($this->l != -1) { + $before = $tokens[$this->l]->getNonsemanticTokensBefore(); + } + + if ($this->r != -1) { + $after = $tokens[$this->r]->getNonsemanticTokensAfter(); + } + + return array($before, $after); + } + + public function getDocblockToken() { + if ($this->l == -1) { + return null; + } + $tokens = $this->tree->getRawTokenStream(); + + for ($ii = $this->l - 1; $ii >= 0; $ii--) { + if ($tokens[$ii]->getTypeName() == 'T_DOC_COMMENT') { + return $tokens[$ii]; + } + if (!$tokens[$ii]->isAnyWhitespace()) { + return null; + } + } + + return null; + } + +} diff --git a/src/staticanalysis/parsers/xhpast/api/node/__init__.php b/src/staticanalysis/parsers/xhpast/api/node/__init__.php new file mode 100644 index 00000000..6e5f237e --- /dev/null +++ b/src/staticanalysis/parsers/xhpast/api/node/__init__.php @@ -0,0 +1,23 @@ +id = $id; + $this->typeID = $type; + $this->offset = $offset; + $this->value = $value; + $this->tree = $tree; + } + + public function getTypeID() { + return $this->typeID; + } + + public function getTypeName() { + static $map; + if (empty($map)) { + $map = xhpast_parser_token_constants(); + } + + $type_id = $this->getTypeID(); + + if ($type_id <= 255) { + return chr($type_id); + } + + if (empty($map[$type_id])) { + throw new Exception("No type name for token type ID '{$type_id}'."); + } + + return $map[$type_id]; + } + + public function getValue() { + return $this->value; + } + + public function getOffset() { + return $this->offset; + } + + public function isComment() { + return ($this->getTypeName() == 'T_COMMENT' || + $this->getTypeName() == 'T_DOC_COMMENT'); + } + + public function isAnyWhitespace() { + return ($this->getTypeName() == 'T_WHITESPACE' || + $this->getTypeName() == 'T_XHP_WHITESPACE'); + } + + public function isSemantic() { + return !($this->isComment() || $this->isAnyWhitespace()); + } + + public function getNonsemanticTokensBefore() { + $tokens = $this->tree->getRawTokenStream(); + $result = array(); + $ii = $this->id - 1; + while ($ii >= 0 && !$tokens[$ii]->isSemantic()) { + $result[$ii] = $tokens[$ii]; + --$ii; + } + return array_reverse($result); + } + + public function getNonsemanticTokensAfter() { + $tokens = $this->tree->getRawTokenStream(); + $result = array(); + $ii = $this->id + 1; + while ($ii < count($tokens) && !$tokens[$ii]->isSemantic()) { + $result[$ii] = $tokens[$ii]; + ++$ii; + } + return $result; + } + + +} diff --git a/src/staticanalysis/parsers/xhpast/api/token/__init__.php b/src/staticanalysis/parsers/xhpast/api/token/__init__.php new file mode 100644 index 00000000..b07c3932 --- /dev/null +++ b/src/staticanalysis/parsers/xhpast/api/token/__init__.php @@ -0,0 +1,12 @@ +resolve()); + } + + public static function newFromDataAndResolvedExecFuture( + $php_source, + array $resolved) { + + list($err, $stdout, $stderr) = $resolved; + if ($err) { + if ($err == 1) { + $matches = null; + $is_syntax = preg_match( + '/^XHPAST Parse Error: (.*) on line (\d+)/', + $stderr, + $matches); + if ($is_syntax) { + throw new XHPASTSyntaxErrorException($matches[2], $stderr); + } + } + throw new Exception("XHPAST failed to parse file data {$err}: {$stderr}"); + } + + $data = json_decode($stdout, true); + if (!is_array($data)) { + throw new Exception("XHPAST: failed to decode tree."); + } + + return new XHPASTTree($data['tree'], $data['stream'], $php_source); + } + + public function __construct(array $tree, array $stream, $source) { + $ii = 0; + $offset = 0; + foreach ($stream as $token) { + $this->stream[$ii] = new XHPASTToken( + $ii, + $token[0], + substr($source, $offset, $token[1]), + $offset, + $this); + $offset += $token[1]; + ++$ii; + } + + $this->buildTree(array($tree)); + } + + public function getRootNode() { + return $this->tree[0]; + } + + protected function buildTree(array $tree) { + $ii = count($this->tree); + $nodes = array(); + foreach ($tree as $node) { + $this->tree[$ii] = new XHPASTNode($ii, $node, $this); + $nodes[$ii] = $node; + ++$ii; + } + foreach ($nodes as $node_id => $node) { + if (isset($node[3])) { + $children = $this->buildTree($node[3]); + foreach ($children as $child) { + $child->setParentNode($this->tree[$node_id]); + } + $this->tree[$node_id]->setChildren($children); + } + } + return array_select_keys($this->tree, array_keys($nodes)); + } + + public function getRawTokenStream() { + return $this->stream; + } + + public function renderAsText() { + return $this->executeRenderAsText(array($this->getRootNode()), 0); + } + + protected function executeRenderAsText($list, $depth) { + $return = ''; + foreach ($list as $node) { + if ($depth) { + $return .= str_repeat(' ', $depth); + } + $return .= $node->getDescription()."\n"; + $return .= $this->executeRenderAsText($node->getChildren(), $depth + 1); + } + return $return; + } + +} diff --git a/src/staticanalysis/parsers/xhpast/api/tree/__init__.php b/src/staticanalysis/parsers/xhpast/api/tree/__init__.php new file mode 100644 index 00000000..affc4112 --- /dev/null +++ b/src/staticanalysis/parsers/xhpast/api/tree/__init__.php @@ -0,0 +1,17 @@ +write($data); + + return $future; +} diff --git a/src/staticanalysis/parsers/xhpast/constants/__init__.php b/src/staticanalysis/parsers/xhpast/constants/__init__.php new file mode 100644 index 00000000..5f45fb4e --- /dev/null +++ b/src/staticanalysis/parsers/xhpast/constants/__init__.php @@ -0,0 +1,20 @@ + 'n_PROGRAM', + 9001 => 'n_SYMBOL_NAME', + 9002 => 'n_HALT_COMPILER', + 9003 => 'n_NAMESPACE', + 9004 => 'n_STATEMENT', + 9005 => 'n_EMPTY', + 9006 => 'n_STATEMENT_LIST', + 9007 => 'n_OPEN_TAG', + 9008 => 'n_CLOSE_TAG', + 9009 => 'n_USE_LIST', + 9010 => 'n_USE', + 9011 => 'n_CONSTANT_DECLARATION_LIST', + 9012 => 'n_CONSTANT_DECLARATION', + 9013 => 'n_STRING', + 9014 => 'n_LABEL', + 9015 => 'n_CONDITION_LIST', + 9016 => 'n_CONTROL_CONDITION', + 9017 => 'n_IF', + 9018 => 'n_ELSEIF', + 9019 => 'n_ELSE', + 9020 => 'n_WHILE', + 9021 => 'n_DO_WHILE', + 9022 => 'n_FOR', + 9023 => 'n_FOR_EXPRESSION', + 9024 => 'n_SWITCH', + 9025 => 'n_BREAK', + 9026 => 'n_CONTINUE', + 9027 => 'n_RETURN', + 9028 => 'n_GLOBAL_DECLARATION_LIST', + 9029 => 'n_GLOBAL_DECLARATION', + 9030 => 'n_STATIC_DECLARATION_LIST', + 9031 => 'n_STATIC_DECLARATION', + 9032 => 'n_ECHO_LIST', + 9033 => 'n_ECHO', + 9034 => 'n_INLINE_HTML', + 9035 => 'n_UNSET_LIST', + 9036 => 'n_UNSET', + 9037 => 'n_FOREACH', + 9038 => 'n_FOREACH_EXPRESSION', + 9039 => 'n_THROW', + 9040 => 'n_GOTO', + 9041 => 'n_TRY', + 9042 => 'n_CATCH_LIST', + 9043 => 'n_CATCH', + 9044 => 'n_DECLARE', + 9045 => 'n_DECLARE_DECLARATION_LIST', + 9046 => 'n_DECLARE_DECLARATION', + 9047 => 'n_VARIABLE', + 9048 => 'n_REFERENCE', + 9049 => 'n_VARIABLE_REFERENCE', + 9050 => 'n_FUNCTION_DECLARATION', + 9051 => 'n_CLASS_DECLARATION', + 9052 => 'n_CLASS_ATTRIBUTES', + 9053 => 'n_EXTENDS', + 9054 => 'n_EXTENDS_LIST', + 9055 => 'n_IMPLEMENTS_LIST', + 9056 => 'n_INTERFACE_DECLARATION', + 9057 => 'n_CASE', + 9058 => 'n_DEFAULT', + 9059 => 'n_DECLARATION_PARAMETER_LIST', + 9060 => 'n_DECLARATION_PARAMETER', + 9061 => 'n_TYPE_NAME', + 9062 => 'n_VARIABLE_VARIABLE', + 9063 => 'n_CLASS_MEMBER_DECLARATION_LIST', + 9064 => 'n_CLASS_MEMBER_DECLARATION', + 9065 => 'n_CLASS_CONSTANT_DECLARATION_LIST', + 9066 => 'n_CLASS_CONSTANT_DECLARATION', + 9067 => 'n_METHOD_DECLARATION', + 9068 => 'n_METHOD_MODIFIER_LIST', + 9069 => 'n_FUNCTION_MODIFIER_LIST', + 9070 => 'n_CLASS_MEMBER_MODIFIER_LIST', + 9071 => 'n_EXPRESSION_LIST', + 9072 => 'n_LIST', + 9073 => 'n_ASSIGNMENT', + 9074 => 'n_NEW', + 9075 => 'n_UNARY_PREFIX_EXPRESSION', + 9076 => 'n_UNARY_POSTFIX_EXPRESSION', + 9077 => 'n_BINARY_EXPRESSION', + 9078 => 'n_TERNARY_EXPRESSION', + 9079 => 'n_CAST_EXPRESSION', + 9080 => 'n_CAST', + 9081 => 'n_OPERATOR', + 9082 => 'n_ARRAY_LITERAL', + 9083 => 'n_EXIT_EXPRESSION', + 9084 => 'n_BACKTICKS_EXPRESSION', + 9085 => 'n_LEXICAL_VARIABLE_LIST', + 9086 => 'n_NUMERIC_SCALAR', + 9087 => 'n_STRING_SCALAR', + 9088 => 'n_MAGIC_SCALAR', + 9089 => 'n_CLASS_STATIC_ACCESS', + 9090 => 'n_CLASS_NAME', + 9091 => 'n_MAGIC_CLASS_KEYWORD', + 9092 => 'n_OBJECT_PROPERTY_ACCESS', + 9093 => 'n_ARRAY_VALUE_LIST', + 9094 => 'n_ARRAY_VALUE', + 9095 => 'n_CALL_PARAMETER_LIST', + 9096 => 'n_VARIABLE_EXPRESSION', + 9097 => 'n_INCLUDE_FILE', + 9098 => 'n_HEREDOC', + 9099 => 'n_FUNCTION_CALL', + 9100 => 'n_INDEX_ACCESS', + 9101 => 'n_ASSIGNMENT_LIST', + 9102 => 'n_METHOD_CALL', + 9103 => 'n_XHP_TAG', + 9104 => 'n_XHP_TAG_OPEN', + 9105 => 'n_XHP_TAG_CLOSE', + 9106 => 'n_XHP_TEXT', + 9107 => 'n_XHP_EXPRESSION', + 9108 => 'n_XHP_ATTRIBUTE_LIST', + 9109 => 'n_XHP_ATTRIBUTE', + 9110 => 'n_XHP_LITERAL', + 9111 => 'n_XHP_ATTRIBUTE_LITERAL', + 9112 => 'n_XHP_ATTRIBUTE_EXPRESSION', + 9113 => 'n_XHP_NODE_LIST', + 9114 => 'n_XHP_ENTITY', + 9115 => 'n_CONCATENATION_LIST', + 9116 => 'n_PARENTHETICAL_EXPRESSION', + ); +} diff --git a/src/staticanalysis/parsers/xhpast/constants/parser_tokens.php b/src/staticanalysis/parsers/xhpast/constants/parser_tokens.php new file mode 100644 index 00000000..a2a44f12 --- /dev/null +++ b/src/staticanalysis/parsers/xhpast/constants/parser_tokens.php @@ -0,0 +1,172 @@ + 'T_REQUIRE_ONCE', + 259 => 'T_REQUIRE', + 260 => 'T_EVAL', + 261 => 'T_INCLUDE_ONCE', + 262 => 'T_INCLUDE', + 263 => 'T_LOGICAL_OR', + 264 => 'T_LOGICAL_XOR', + 265 => 'T_LOGICAL_AND', + 266 => 'T_PRINT', + 267 => 'T_SR_EQUAL', + 268 => 'T_SL_EQUAL', + 269 => 'T_XOR_EQUAL', + 270 => 'T_OR_EQUAL', + 271 => 'T_AND_EQUAL', + 272 => 'T_MOD_EQUAL', + 273 => 'T_CONCAT_EQUAL', + 274 => 'T_DIV_EQUAL', + 275 => 'T_MUL_EQUAL', + 276 => 'T_MINUS_EQUAL', + 277 => 'T_PLUS_EQUAL', + 278 => 'T_BOOLEAN_OR', + 279 => 'T_BOOLEAN_AND', + 280 => 'T_IS_NOT_IDENTICAL', + 281 => 'T_IS_IDENTICAL', + 282 => 'T_IS_NOT_EQUAL', + 283 => 'T_IS_EQUAL', + 284 => 'T_IS_GREATER_OR_EQUAL', + 285 => 'T_IS_SMALLER_OR_EQUAL', + 286 => 'T_SR', + 287 => 'T_SL', + 288 => 'T_INSTANCEOF', + 289 => 'T_UNSET_CAST', + 290 => 'T_BOOL_CAST', + 291 => 'T_OBJECT_CAST', + 292 => 'T_ARRAY_CAST', + 293 => 'T_BINARY_CAST', + 294 => 'T_UNICODE_CAST', + 295 => 'T_STRING_CAST', + 296 => 'T_DOUBLE_CAST', + 297 => 'T_INT_CAST', + 298 => 'T_DEC', + 299 => 'T_INC', + 300 => 'T_CLONE', + 301 => 'T_NEW', + 302 => 'T_EXIT', + 303 => 'T_IF', + 304 => 'T_ELSEIF', + 305 => 'T_ELSE', + 306 => 'T_ENDIF', + 307 => 'T_LNUMBER', + 308 => 'T_DNUMBER', + 309 => 'T_STRING', + 310 => 'T_STRING_VARNAME', + 311 => 'T_VARIABLE', + 312 => 'T_NUM_STRING', + 313 => 'T_INLINE_HTML', + 314 => 'T_CHARACTER', + 315 => 'T_BAD_CHARACTER', + 316 => 'T_ENCAPSED_AND_WHITESPACE', + 317 => 'T_CONSTANT_ENCAPSED_STRING', + 318 => 'T_BACKTICKS_EXPR', + 319 => 'T_ECHO', + 320 => 'T_DO', + 321 => 'T_WHILE', + 322 => 'T_ENDWHILE', + 323 => 'T_FOR', + 324 => 'T_ENDFOR', + 325 => 'T_FOREACH', + 326 => 'T_ENDFOREACH', + 327 => 'T_DECLARE', + 328 => 'T_ENDDECLARE', + 329 => 'T_AS', + 330 => 'T_SWITCH', + 331 => 'T_ENDSWITCH', + 332 => 'T_CASE', + 333 => 'T_DEFAULT', + 334 => 'T_BREAK', + 335 => 'T_CONTINUE', + 336 => 'T_GOTO', + 337 => 'T_FUNCTION', + 338 => 'T_CONST', + 339 => 'T_RETURN', + 340 => 'T_TRY', + 341 => 'T_CATCH', + 342 => 'T_THROW', + 343 => 'T_USE', + 344 => 'T_GLOBAL', + 345 => 'T_PUBLIC', + 346 => 'T_PROTECTED', + 347 => 'T_PRIVATE', + 348 => 'T_FINAL', + 349 => 'T_ABSTRACT', + 350 => 'T_STATIC', + 351 => 'T_VAR', + 352 => 'T_UNSET', + 353 => 'T_ISSET', + 354 => 'T_EMPTY', + 355 => 'T_HALT_COMPILER', + 356 => 'T_CLASS', + 357 => 'T_INTERFACE', + 358 => 'T_EXTENDS', + 359 => 'T_IMPLEMENTS', + 360 => 'T_OBJECT_OPERATOR', + 361 => 'T_DOUBLE_ARROW', + 362 => 'T_LIST', + 363 => 'T_ARRAY', + 364 => 'T_CLASS_C', + 365 => 'T_METHOD_C', + 366 => 'T_FUNC_C', + 367 => 'T_LINE', + 368 => 'T_FILE', + 369 => 'T_COMMENT', + 370 => 'T_DOC_COMMENT', + 371 => 'T_OPEN_TAG', + 372 => 'T_OPEN_TAG_WITH_ECHO', + 373 => 'T_OPEN_TAG_FAKE', + 374 => 'T_CLOSE_TAG', + 375 => 'T_WHITESPACE', + 376 => 'T_START_HEREDOC', + 377 => 'T_END_HEREDOC', + 378 => 'T_HEREDOC', + 379 => 'T_DOLLAR_OPEN_CURLY_BRACES', + 380 => 'T_CURLY_OPEN', + 381 => 'T_PAAMAYIM_NEKUDOTAYIM', + 382 => 'T_BINARY_DOUBLE', + 383 => 'T_BINARY_HEREDOC', + 384 => 'T_NAMESPACE', + 385 => 'T_NS_C', + 386 => 'T_DIR', + 387 => 'T_NS_SEPARATOR', + 388 => 'T_XHP_WHITESPACE', + 389 => 'T_XHP_TEXT', + 390 => 'T_XHP_LT_DIV', + 391 => 'T_XHP_LT_DIV_GT', + 392 => 'T_XHP_ATTRIBUTE', + 393 => 'T_XHP_CATEGORY', + 394 => 'T_XHP_CHILDREN', + 395 => 'T_XHP_ANY', + 396 => 'T_XHP_EMPTY', + 397 => 'T_XHP_PCDATA', + 398 => 'T_XHP_COLON', + 399 => 'T_XHP_HYPHEN', + 400 => 'T_XHP_BOOLEAN', + 401 => 'T_XHP_NUMBER', + 402 => 'T_XHP_ARRAY', + 403 => 'T_XHP_STRING', + 404 => 'T_XHP_ENUM', + 405 => 'T_XHP_FLOAT', + 406 => 'T_XHP_REQUIRED', + 407 => 'T_XHP_ENTITY', + ); +} diff --git a/src/unit/engine/base/ArcanistBaseUnitTestEngine.php b/src/unit/engine/base/ArcanistBaseUnitTestEngine.php new file mode 100644 index 00000000..7ec25c5e --- /dev/null +++ b/src/unit/engine/base/ArcanistBaseUnitTestEngine.php @@ -0,0 +1,50 @@ +workingCopy = $working_copy; + return $this; + } + + final public function getWorkingCopy() { + return $this->workingCopy; + } + + final public function setPaths(array $paths) { + $this->paths = $paths; + return $this; + } + + final public function getPaths() { + return $this->paths; + } + + abstract public function run(); + +} diff --git a/src/unit/engine/base/__init__.php b/src/unit/engine/base/__init__.php new file mode 100644 index 00000000..69d1e77a --- /dev/null +++ b/src/unit/engine/base/__init__.php @@ -0,0 +1,10 @@ +getPaths() as $path) { + $library_root = phutil_get_library_root_for_path($path); + if (!$library_root) { + continue; + } + $library_name = phutil_get_library_name_for_root($library_root); + + $path = Filesystem::resolvePath($path); + if ($path == $library_root) { + continue; + } + + if (!is_dir($path)) { + $path = dirname($path); + } + + $library_path = Filesystem::readablePath($path, $library_root); + if (basename($library_path) == '__tests__') { + // Okay, this is a __tests__ module. + } else { + if (phutil_module_exists($library_name, $library_path.'/__tests__')) { + // This is a module which has a __tests__ module in it. + $path .= '/__tests__'; + } else { + // Look for a parent named __tests__. + $rpos = strrpos($library_path, '/__tests__'); + if ($rpos === false) { + // No tests to run since there is no child or parent module named + // __tests__. + continue; + } + // Select the parent named __tests__. + $path = substr($path, 0, $rpos + strlen('/__tests__')); + } + } + + + $module_name = Filesystem::readablePath($path, $library_root); + $module_key = $library_name.':'.$module_name; + $tests[$module_key] = array( + 'library' => $library_name, + 'root' => $library_root, + 'module' => $module_name, + ); + } + + if (!$tests) { + throw new ArcanistNoEffectException("No tests to run."); + } + + $run_tests = array(); + $all_test_classes = phutil_find_class_descendants('ArcanistPhutilTestCase'); + $all_test_classes = array_fill_keys($all_test_classes, true); + foreach ($tests as $test) { + $local_classes = phutil_find_classes_declared_in_module( + $test['library'], + $test['module']); + $local_classes = array_fill_keys($local_classes, true); + $run_tests += array_intersect($local_classes, $all_test_classes); + } + $run_tests = array_keys($run_tests); + + if (!$run_tests) { + throw new ArcanistNoEffectException( + "No tests to run. You may need to rebuild the phutil library map."); + } + + $results = array(); + foreach ($run_tests as $test_class) { + phutil_autoload_class($test_class); + $test_case = newv($test_class, array()); + $results[] = $test_case->run(); + } + if ($results) { + $results = call_user_func_array('array_merge', $results); + } + + + return $results; + } + +} diff --git a/src/unit/engine/phutil/__init__.php b/src/unit/engine/phutil/__init__.php new file mode 100644 index 00000000..11320952 --- /dev/null +++ b/src/unit/engine/phutil/__init__.php @@ -0,0 +1,18 @@ +failTest($message); + throw new ArcanistPhutilTestTerminatedException(); + } + + final protected function assertFailure($message) { + $this->failTest($message); + throw new ArcanistPhutilTestTerminatedException(); + } + + final private function failTest($reason) { + $result = new ArcanistUnitTestResult(); + $result->setName($this->runningTest); + $result->setResult(ArcanistUnitTestResult::RESULT_FAIL); + $result->setUserData($reason); + $this->results[] = $result; + } + + final private function passTest($reason) { + $result = new ArcanistUnitTestResult(); + $result->setName($this->runningTest); + $result->setResult(ArcanistUnitTestResult::RESULT_PASS); + $result->setUserData($reason); + $this->results[] = $result; + } + + final public function run() { + $this->results = array(); + + $reflection = new ReflectionClass($this); + foreach ($reflection->getMethods() as $method) { + $name = $method->getName(); + if (preg_match('/^test/', $name)) { + $this->runningTest = $name; + try { + call_user_func_array( + array($this, $name), + array()); + $this->passTest("All assertions passed."); + } catch (ArcanistPhutilTestTerminatedException $ex) { + // Continue with the next test. + } catch (Exception $ex) { + $this->failTest($ex->getMessage()); + } + } + } + + return $this->results; + } + +} diff --git a/src/unit/engine/phutil/testcase/__init__.php b/src/unit/engine/phutil/testcase/__init__.php new file mode 100644 index 00000000..7f7dbd12 --- /dev/null +++ b/src/unit/engine/phutil/testcase/__init__.php @@ -0,0 +1,13 @@ +name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setResult($result) { + $this->result = $result; + return $this; + } + + public function getResult() { + return $this->result; + } + + public function setUserData($user_data) { + $this->userData = $user_data; + return $this; + } + + public function getUserData() { + return $this->userData; + } + +} diff --git a/src/unit/result/__init__.php b/src/unit/result/__init__.php new file mode 100644 index 00000000..21aa6652 --- /dev/null +++ b/src/unit/result/__init__.php @@ -0,0 +1,10 @@ + array( + 'help' => + "Show the amended commit message." + ), + 'revision' => array( + 'param' => 'revision_id', + 'help' => + "Amend a specific revision. If you do not specify a revision, ". + "arc will look in the commit message at HEAD.", + ), + ); + } + + public function run() { + $repository_api = $this->getRepositoryAPI(); + if (!($repository_api instanceof ArcanistGitAPI)) { + throw new ArcanistUsageException( + "You may only run 'arc amend' in a git working copy."); + } + + if ($repository_api->getUncommittedChanges()) { + throw new ArcanistUsageException( + "You have uncommitted changes in this branch. Stage and commit (or ". + "revert) them before proceeding."); + } + + if ($this->getArgument('revision')) { + $revision_id = $this->getArgument('revision'); + } else { + $log = $repository_api->getGitCommitLog(); + $parser = new ArcanistDiffParser(); + $changes = $parser->parseDiff($log); + if (count($changes) != 1) { + throw new Exception("Expected one log."); + } + $change = reset($changes); + if ($change->getType() != ArcanistDiffChangeType::TYPE_MESSAGE) { + throw new Exception("Expected message change."); + } + $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( + $change->getMetadata('message')); + $revision_id = $message->getRevisionID(); + if (!$revision_id) { + throw new ArcanistUsageException( + "No revision specified with '--revision', and no Differential ". + "revision marker in HEAD."); + } + } + + $conduit = $this->getConduit(); + $message = $conduit->callMethodSynchronous( + 'differential.getcommitmessage', + array( + 'revision_id' => $revision_id, + )); + + if ($this->getArgument('show')) { + echo $message."\n"; + } else { + $repository_api->amendGitHeadCommit($message); + echo "Amended commit message.\n"; + } + + $working_copy = $this->getWorkingCopy(); + $remote_hooks = $working_copy->getConfig('remote_hooks_installed', false); + if (!$remote_hooks) { + echo "According to .arcconfig, remote commit hooks are not installed ". + "for this project, so the revision will be marked committed now. ". + "Consult the documentation for instructions on installing hooks.". + "\n\n"; + $mark_workflow = $this->buildChildWorkflow( + 'mark-committed', + array($revision_id)); + $mark_workflow->run(); + } + + return 0; + } +} diff --git a/src/workflow/amend/__init__.php b/src/workflow/amend/__init__.php new file mode 100644 index 00000000..4a6cbc24 --- /dev/null +++ b/src/workflow/amend/__init__.php @@ -0,0 +1,18 @@ +arcanistConfiguration = $arcanist_configuration; + return $this; + } + + public function getArcanistConfiguration() { + return $this->arcanistConfiguration; + } + + public function getCommandHelp() { + return get_class($this).": Undocumented"; + } + + public function requiresWorkingCopy() { + return false; + } + + public function requiresConduit() { + return false; + } + + public function requiresAuthentication() { + return false; + } + + public function requiresRepositoryAPI() { + return false; + } + + public function setCommand($command) { + $this->command = $command; + return $this; + } + + public function getCommand() { + return $this->command; + } + + public function setUserName($user_name) { + $this->userName = $user_name; + return $this; + } + + public function getUserName() { + return $this->userName; + } + + public function getArguments() { + return array(); + } + + private function setParentWorkflow($parent_workflow) { + $this->parentWorkflow = $parent_workflow; + return $this; + } + + protected function getParentWorkflow() { + return $this->parentWorkflow; + } + + public function buildChildWorkflow($command, array $argv) { + $arc_config = $this->getArcanistConfiguration(); + $workflow = $arc_config->buildWorkflow($command); + $workflow->setParentWorkflow($this); + $workflow->setCommand($command); + + if ($this->repositoryAPI) { + $workflow->setRepositoryAPI($this->repositoryAPI); + } + + if ($this->userGUID) { + $workflow->setUserGUID($this->getUserGUID()); + $workflow->setUserName($this->getUserName()); + } + + if ($this->conduit) { + $workflow->setConduit($this->conduit); + } + + if ($this->workingCopy) { + $workflow->setWorkingCopy($this->workingCopy); + } + + $workflow->setArcanistConfiguration($arc_config); + + $workflow->parseArguments(array_values($argv)); + + return $workflow; + } + + public function getArgument($key, $default = null) { + $args = $this->arguments; + if (!array_key_exists($key, $args)) { + return $default; + } + return $args[$key]; + } + + final public function getCompleteArgumentSpecification() { + $spec = $this->getArguments(); + $arc_config = $this->getArcanistConfiguration(); + $command = $this->getCommand(); + $spec += $arc_config->getCustomArgumentsForCommand($command); + return $spec; + } + + public function parseArguments(array $args) { + + $spec = $this->getCompleteArgumentSpecification(); + + $dict = array(); + + $more_key = null; + if (!empty($spec['*'])) { + $more_key = $spec['*']; + unset($spec['*']); + $dict[$more_key] = array(); + } + + $short_to_long_map = array(); + foreach ($spec as $long => $options) { + if (!empty($options['short'])) { + $short_to_long_map[$options['short']] = $long; + } + } + + $more = array(); + for ($ii = 0; $ii < count($args); $ii++) { + $arg = $args[$ii]; + $arg_name = null; + $arg_key = null; + if ($arg == '--') { + $more = array_merge( + $more, + array_slice($args, $ii + 1)); + break; + } else if (!strncmp($arg, '--', 2)) { + $arg_key = substr($arg, 2); + if (!array_key_exists($arg_key, $spec)) { + throw new ArcanistUsageException( + "Unknown argument '{$arg_key}'. Try 'arc help'."); + } + } else if (!strncmp($arg, '-', 1)) { + $arg_key = substr($arg, 1); + if (empty($short_to_long_map[$arg_key])) { + throw new ArcanistUsageException( + "Unknown argument '{$arg_key}'. Try 'arc help'."); + } + $arg_key = $short_to_long_map[$arg_key]; + } else { + $more[] = $arg; + continue; + } + + $options = $spec[$arg_key]; + if (empty($options['param'])) { + $dict[$arg_key] = true; + } else { + if ($ii == count($args) - 1) { + throw new ArcanistUsageException( + "Option '{$arg}' requires a parameter."); + } + $dict[$arg_key] = $args[$ii + 1]; + $ii++; + } + } + + if ($more) { + if ($more_key) { + $dict[$more_key] = $more; + } else { + $example = reset($more); + throw new ArcanistUsageException( + "Unrecognized argument '{$example}'. Try 'arc help'."); + } + } + + foreach ($dict as $key => $value) { + if (empty($spec[$key]['conflicts'])) { + continue; + } + foreach ($spec[$key]['conflicts'] as $conflict => $more) { + if (isset($dict[$conflict])) { + if ($more) { + $more = ': '.$more; + } else { + $more = '.'; + } + // TODO: We'll always display these as long-form, when the user might + // have typed them as short form. + throw new ArcanistUsageException( + "Arguments '--{$key}' and '--{$conflict}' are mutually exclusive". + $more); + } + } + } + + $this->arguments = $dict; + + $this->didParseArguments(); + + return $this; + } + + protected function didParseArguments() { + // Override this to customize workflow argument behavior. + } + + public function getWorkingCopy() { + if (!$this->workingCopy) { + $workflow = get_class($this); + throw new Exception( + "This workflow ('{$workflow}') requires a working copy, override ". + "requiresWorkingCopy() to return true."); + } + return $this->workingCopy; + } + + public function setWorkingCopy( + ArcanistWorkingCopyIdentity $working_copy) { + $this->workingCopy = $working_copy; + return $this; + } + + public function getConduit() { + if (!$this->conduit) { + $workflow = get_class($this); + throw new Exception( + "This workflow ('{$workflow}') requires a Conduit, override ". + "requiresConduit() to return true."); + } + return $this->conduit; + } + + public function setConduit(ConduitClient $conduit) { + $this->conduit = $conduit; + return $this; + } + + public function getUserGUID() { + if (!$this->userGUID) { + $workflow = get_class($this); + throw new Exception( + "This workflow ('{$workflow}') requires authentication, override ". + "requiresAuthentication() to return true."); + } + return $this->userGUID; + } + + public function setUserGUID($guid) { + $this->userGUID = $guid; + return $this; + } + + public function setRepositoryAPI($api) { + $this->repositoryAPI = $api; + return $this; + } + + public function getRepositoryAPI() { + if (!$this->repositoryAPI) { + $workflow = get_class($this); + throw new Exception( + "This workflow ('{$workflow}') requires a Repository API, override ". + "requiresRepositoryAPI() to return true."); + } + return $this->repositoryAPI; + } + + protected function shouldRequireCleanUntrackedFiles() { + return empty($this->arguments['allow-untracked']); + } + + protected function requireCleanWorkingCopy() { + $api = $this->getRepositoryAPI(); + + $untracked = $api->getUntrackedChanges(); + if ($this->shouldRequireCleanUntrackedFiles()) { + if (!empty($untracked)) { + throw new ArcanistUsageException( + "You have untracked files in this working copy:\n". + " ".implode("\n ", $untracked)."\n\n". + "Add or delete them before proceeding, or include them in your ". + "ignore rules. To bypass this check, use --allow-untracked."); + } + } + + + if ($api->getMergeConflicts()) { + throw new ArcanistUsageException( + "You have merge conflicts in this working copy. Resolve merge ". + "conflicts before proceeding."); + } + + if ($api->getUnstagedChanges()) { + throw new ArcanistUsageException( + "You have unstaged changes in this branch. Stage and commit (or ". + "revert) them before proceeding."); + } + + if ($api->getUncommittedChanges()) { + throw new ArcanistUsageException( + "You have uncommitted changes in this branch. Commit (or revert) them ". + "before proceeding."); + } + } + + protected function chooseRevision( + array $revision_data, + $revision_id, + $prompt = null) { + + $revisions = array(); + foreach ($revision_data as $data) { + $ref = ArcanistDifferentialRevisionRef::newFromDictionary($data); + $revisions[$ref->getID()] = $ref; + } + + if ($revision_id) { + $revision_id = $this->normalizeRevisionID($revision_id); + if (empty($revisions[$revision_id])) { + throw new ArcanistChooseInvalidRevisionException(); + } + return $revisions[$revision_id]; + } + + if (!count($revisions)) { + throw new ArcanistChooseNoRevisionsException(); + } + + $repository_api = $this->getRepositoryAPI(); + + $candidates = array(); + $cur_path = $repository_api->getPath(); + foreach ($revisions as $revision) { + $source_path = $revision->getSourcePath(); + if ($source_path == $cur_path) { + $candidates[] = $revision; + } + } + + if (count($candidates) == 1) { + $candidate = reset($candidates); + $revision_id = $candidate->getID(); + } + + if ($revision_id) { + return $revisions[$revision_id]; + } + + $revision_indexes = array_keys($revisions); + + echo "\n"; + $ii = 1; + foreach ($revisions as $revision) { + echo ' ['.$ii++.'] D'.$revision->getID().' '.$revision->getName()."\n"; + } + + while (true) { + $id = phutil_console_prompt($prompt); + $id = trim(strtoupper($id), 'D'); + if (isset($revisions[$id])) { + return $revisions[$id]; + } + if (isset($revision_indexes[$id - 1])) { + return $revisions[$revision_indexes[$id - 1]]; + } + } + } + + protected function loadDiffBundleFromConduit( + ConduitClient $conduit, + $diff_id) { + + return $this->loadBundleFromConduit( + $conduit, + array( + 'diff_id' => $diff_id, + )); + } + + protected function loadRevisionBundleFromConduit( + ConduitClient $conduit, + $revision_id) { + + return $this->loadBundleFromConduit( + $conduit, + array( + 'revision_id' => $revision_id, + )); + } + + private function loadBundleFromConduit( + ConduitClient $conduit, + $params) { + + $future = $conduit->callMethod('differential.getdiff', $params); + $diff = $future->resolve(); + + $changes = array(); + foreach ($diff['changes'] as $changedict) { + $changes[] = ArcanistDiffChange::newFromDictionary($changedict); + } + $bundle = ArcanistBundle::newFromChanges($changes); + return $bundle; + } + + protected function getChangedLines($path, $mode) { + if (is_dir($path)) { + return array(); + } + + $change = $this->getChange($path); + $lines = $change->getChangedLines($mode); + + return array_keys($lines); + } + + private function getChange($path) { + $repository_api = $this->getRepositoryAPI(); + + if ($repository_api instanceof ArcanistSubversionAPI) { + if (empty($this->changeCache[$path])) { + $diff = $repository_api->getRawDiffText($path); + $parser = new ArcanistDiffParser(); + $changes = $parser->parseDiff($diff); + if (count($changes) != 1) { + throw new Exception("Expected exactly one change."); + } + $this->changeCache[$path] = reset($changes); + } + } else { + if (empty($this->changeCache)) { + $diff = $repository_api->getFullGitDiff(); + $parser = new ArcanistDiffParser(); + $changes = $parser->parseDiff($diff); + foreach ($changes as $change) { + $this->changeCache[$change->getCurrentPath()] = $change; + } + } + } + + if (empty($this->changeCache[$path])) { + // TODO: This can legitimately occur under git if you make a change, + // "git commit" it, and then revert the change in the working copy and + // run "arc lint". We should probably just make a dummy, empty changeset + // in this case, at least under git. + throw new Exception( + "Trying to get change for unchanged path '{$path}'!"); + } + + return $this->changeCache[$path]; + } + + final public function willRunWorkflow() { + $spec = $this->getCompleteArgumentSpecification(); + foreach ($this->arguments as $arg => $value) { + if (empty($spec[$arg])) { + continue; + } + $options = $spec[$arg]; + if (!empty($options['supports'])) { + $system_name = $this->getRepositoryAPI()->getSourceControlSystemName(); + if (!in_array($system_name, $options['supports'])) { + $extended_info = null; + if (!empty($options['nosupport'][$system_name])) { + $extended_info = ' '.$options['nosupport'][$system_name]; + } + throw new ArcanistUsageException( + "Option '--{$arg}' is not supported under {$system_name}.". + $extended_info); + } + } + } + } + + protected function parseGitRelativeCommit(ArcanistGitAPI $api, array $argv) { + if (count($argv) == 0) { + return; + } + if (count($argv) != 1) { + throw new ArcanistUsageException( + "Specify exactly one commit."); + } + $base = reset($argv); + if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) { + $merge_base = $base; + } else { + list($err, $merge_base) = exec_manual( + '(cd %s; git merge-base %s HEAD)', + $api->getPath(), + $base); + if ($err) { + throw new ArcanistUsageException( + "Unable to parse git commit name '{$base}'."); + } + } + $api->setRelativeCommit(trim($merge_base)); + } + + protected function normalizeRevisionID($revision_id) { + return ltrim(strtoupper($revision_id), 'D'); + } + +} diff --git a/src/workflow/base/__init__.php b/src/workflow/base/__init__.php new file mode 100644 index 00000000..5be4f028 --- /dev/null +++ b/src/workflow/base/__init__.php @@ -0,0 +1,21 @@ + array( + 'help' => + "Show the command which would be issued, but do not actually ". + "commit anything." + ), + 'revision' => array( + 'param' => 'revision_id', + 'help' => + "Commit a specific revision. If you do not specify a revision, ". + "arc will look for committable revisions.", + ) + ); + } + + public function run() { + $repository_api = $this->getRepositoryAPI(); + $conduit = $this->getConduit(); + + $revision_data = $conduit->callMethodSynchronous( + 'differential.find', + array( + 'query' => 'committable', + 'guids' => array( + $this->getUserGUID(), + ), + )); + + try { + $revision_id = $this->getArgument('revision'); + $revision = $this->chooseRevision( + $revision_data, + $revision_id, + 'Which revision do you want to commit?'); + } catch (ArcanistChooseInvalidRevisionException $ex) { + throw new ArcanistUsageException( + "Revision D{$revision_id} is not committable. You can only commit ". + "revisions you own which have been 'accepted'."); + } catch (ArcanistChooseNoRevisionsException $ex) { + throw new ArcanistUsageException( + "You have no committable Differential revisions. You can only commit ". + "revisions you own which have been 'accepted'."); + } + + $revision_id = $revision->getID(); + $revision_name = $revision->getName(); + + $message = $conduit->callMethodSynchronous( + 'differential.getcommitmessage', + array( + 'revision_id' => $revision_id, + )); + + if ($this->getArgument('show')) { + echo $message; + return 0; + } + + echo "Committing D{$revision_id} '{$revision_name}'...\n"; + + $files = $this->getCommitFileList($revision); + + $files = implode(' ', array_map('escapeshellarg', $files)); + $message = escapeshellarg($message); + $root = escapeshellarg($repository_api->getPath()); + + // Specify LANG explicitly so that UTF-8 commit messages don't break + // subversion. + $command = + "(cd {$root} && LANG=en_US.utf8 svn commit {$files} -m {$message})"; + + $err = null; + passthru($command, $err); + + if ($err) { + throw new Exception("Executing 'svn commit' failed!"); + } + + $working_copy = $this->getWorkingCopy(); + $remote_hooks = $working_copy->getConfig('remote_hooks_installed', false); + if (!$remote_hooks) { + echo "According to .arcconfig, remote commit hooks are not installed ". + "for this project, so the revision will be marked committed now. ". + "Consult the documentation for instructions on installing hooks.". + "\n\n"; + $mark_workflow = $this->buildChildWorkflow( + 'mark-committed', + array($revision_id)); + $mark_workflow->run(); + } + + return $err; + } + + protected function getCommitFileList( + ArcanistDifferentialRevisionRef $revision) { + $repository_api = $this->getRepositoryAPI(); + + if (!($repository_api instanceof ArcanistSubversionAPI)) { + throw new ArcanistUsageException( + "arc commit is only supported under SVN. Use arc amend under git."); + } + + $conduit = $this->getConduit(); + + $revision_id = $revision->getID(); + + $revision_source = $revision->getSourcePath(); + $working_copy = $repository_api->getPath(); + if ($revision_source != $working_copy) { + $prompt = + "Revision was generated from '{$revision_source}', but the current ". + "working copy root is '{$working_copy}'. Commit anyway?"; + if (!phutil_console_confirm($prompt)) { + throw new ArcanistUserAbortException(); + } + } + + $commit_paths = $conduit->callMethodSynchronous( + 'differential.getcommitpaths', + array( + 'revision_id' => $revision_id, + )); + $commit_paths = array_fill_keys($commit_paths, true); + + $status = $repository_api->getSVNStatus(); + + $modified_but_not_included = array(); + foreach ($status as $path => $mask) { + if (!empty($commit_paths[$path])) { + continue; + } + foreach ($commit_paths as $will_commit => $ignored) { + if (Filesystem::isDescendant($path, $will_commit)) { + throw new ArcanistUsageException( + "This commit includes the directory '{$will_commit}', but ". + "it contains a modified path ('{$path}') which is NOT included ". + "in the commit. Subversion can not handle this operation and ". + "will commit the path anyway. You need to sort out the working ". + "copy changes to '{$path}' before you may proceed with the ". + "commit."); + } + } + $modified_but_not_included[] = $path; + } + + if ($modified_but_not_included) { + if (count($modified_but_not_included) == 1) { + $prefix = "A locally modified path is not included in this revision:"; + $prompt = "It will NOT be committed. Commit this revision anyway?"; + } else { + $prefix = "Locally modified paths are not included in this revision:"; + $prompt = "They will NOT be committed. Commit this revision anyway?"; + } + $this->promptFileWarning($prefix, $prompt, $modified_but_not_included); + } + + $do_not_exist = array(); + foreach ($commit_paths as $path => $ignored) { + $disk_path = $repository_api->getPath($path); + if (file_exists($disk_path)) { + continue; + } + if (is_link($disk_path)) { + continue; + } + if (idx($status, $path) & ArcanistRepositoryAPI::FLAG_DELETED) { + continue; + } + $do_not_exist[] = $path; + unset($commit_paths[$path]); + } + + if ($do_not_exist) { + if (count($do_not_exist) == 1) { + $prefix = "Revision includes changes to a path that does not exist:"; + $prompt = "Commit this revision anyway?"; + } else { + $prefix = "Revision includes changes to paths that do not exist:"; + $prompt = "Commit this revision anyway?"; + } + $this->promptFileWarning($prefix, $prompt, $do_not_exist); + } + + $files = array_keys($commit_paths); + + if (empty($files)) { + throw new ArcanistUsageException( + "There is nothing left to commit. None of the modified paths exist."); + } + + return $files; + } + + protected function promptFileWarning($prefix, $prompt, array $paths) { + echo $prefix."\n\n"; + foreach ($paths as $path) { + echo " ".$path."\n"; + } + if (!phutil_console_confirm($prompt)) { + throw new ArcanistUserAbortException(); + } + } + +} diff --git a/src/workflow/commit/__init__.php b/src/workflow/commit/__init__.php new file mode 100644 index 00000000..e325c852 --- /dev/null +++ b/src/workflow/commit/__init__.php @@ -0,0 +1,19 @@ +getRepositoryAPI(); + + $paths = $repository_api->getWorkingCopyStatus(); + + foreach ($paths as $path => $status) { + if (is_dir($path)) { + unset($paths[$path]); + } + if ($status & ArcanistRepositoryAPI::FLAG_UNTRACKED) { + unset($paths[$path]); + } + if ($status & ArcanistRepositoryAPI::FLAG_ADDED) { + unset($paths[$path]); + } + } + + $paths = array_keys($paths); + + if (!$paths) { + throw new ArcanistNoEffectException( + "You're covered, you didn't change anything."); + } + + $changed = array(); + foreach ($paths as $path) { + $changed[$path] = $this->getChangedLines($path, 'cover'); + } + + $covers = array(); + foreach ($paths as $path) { + $blame = $repository_api->getBlame($path); + $lines = $changed[$path]; + foreach ($lines as $line) { + list($author, $revision) = idx($blame, $line, array(null, null)); + if (!$author) { + continue; + } + if (!isset($covers[$author])) { + $covers[$author] = array(); + } + if (!isset($covers[$author][$path])) { + $covers[$author][$path] = array( + 'lines' => array(), + 'revisions' => array(), + ); + } + $covers[$author][$path]['lines'][] = $line; + $covers[$author][$path]['revisions'][] = $revision; + } + } + + if (count($covers)) { + foreach ($covers as $author => $files) { + echo phutil_console_format( + "**%s**\n", + $author); + foreach ($files as $file => $info) { + $line_noun = count($info['lines']) == 1 ? 'line' : 'lines'; + $lines = $this->readableSequenceFromLineNumbers($info['lines']); + echo " {$file}: {$line_noun} {$lines}\n"; + } + } + } else { + echo "You're covered, your changes didn't touch anyone else's code.\n"; + } + + return 0; + } + + private function readableSequenceFromLineNumbers(array $array) { + $sequence = array(); + $last = null; + $seq = null; + $array = array_unique(array_map('intval', $array)); + sort($array); + foreach ($array as $element) { + if ($seq !== null && $element == ($seq + 1)) { + $seq++; + continue; + } + + if ($seq === null) { + $last = $element; + $seq = $element; + continue; + } + + if ($seq > $last) { + $sequence[] = $last.'-'.$seq; + } else { + $sequence[] = $last; + } + + $last = $element; + $seq = $element; + } + if ($last !== null && $seq > $last) { + $sequence[] = $last.'-'.$seq; + } else if ($last !== null) { + $sequence[] = $element; + } + + return implode(', ', $sequence); + } + +} diff --git a/src/workflow/cover/__init__.php b/src/workflow/cover/__init__.php new file mode 100644 index 00000000..7a429a40 --- /dev/null +++ b/src/workflow/cover/__init__.php @@ -0,0 +1,17 @@ + array( + 'short' => 'm', + 'supports' => array( + 'git', + ), + 'nosupport' => array( + 'svn' => 'Edit revisions via the web interface when using SVN.', + ), + 'param' => 'message', + 'help' => + "When updating a revision under git, use the specified message ". + "instead of prompting.", + ), + 'edit' => array( + 'supports' => array( + 'git', + ), + 'nosupport' => array( + 'svn' => 'Edit revisions via the web interface when using SVN.', + ), + 'help' => + "When updating a revision under git, edit revision information ". + "before updating.", + ), + 'nounit' => array( + 'help' => + "Do not run unit tests.", + ), + 'nolint' => array( + 'help' => + "Do not run lint.", + 'conflicts' => array( + 'lintall' => '--nolint suppresses lint.', + 'advice' => '--nolint suppresses lint.', + ), + ), + 'only' => array( + 'help' => + "Only generate a diff, without running lint, unit tests, or other ". + "auxiliary steps.", + 'conflicts' => array( + 'preview' => null, + 'message' => '--only does not affect revisions.', + 'edit' => '--only does not affect revisions.', + 'lintall' => '--only suppresses lint.', + 'advice' => '--only suppresses lint.', + 'nounit' => '--only implies --nounit.', + 'nolint' => '--only implies --nolint.', + ), + ), + 'preview' => array( + 'supports' => array( + 'git', + ), + 'nosupport' => array( + 'svn' => 'Revisions are never created directly when using SVN.', + ), + 'help' => + "Instead of creating or updating a revision, only create a diff, ". + "which you may later attach to a revision. This still runs lint ". + "unit tests. See also --only.", + 'conflicts' => array( + 'only' => null, + 'edit' => '--preview does affect revisions.', + 'message' => '--preview does not update any revision.', + ), + ), + 'allow-untracked' => array( + 'help' => + "Skip checks for untracked files in the working copy.", + ), + 'less-context' => array( + 'help' => + "Normally, files are diffed with full context: the entire file is ". + "sent to Differential so reviewers can 'show more' and see it. If ". + "you are making changes to very large files with tens of thousands ". + "of lines, this may not work well. With this flag, a diff will ". + "be created that has only a few lines of context.", + ), + 'lintall' => array( + 'help' => + "Raise all lint warnings, not just those on lines you changed.", + ), + 'advice' => array( + 'help' => + "Raise lint advice in addition to lint warnings and errors.", + ), + '*' => 'paths', + ); + } + + public function run() { + $repository_api = $this->getRepositoryAPI(); + + if ($this->getArgument('less-context')) { + $repository_api->setDiffLinesOfContext(3); + } + + $conduit = $this->getConduit(); + $this->requireCleanWorkingCopy(); + + $parent = null; + + $base_revision = $repository_api->getSourceControlBaseRevision(); + $base_path = $repository_api->getSourceControlPath(); + if ($repository_api instanceof ArcanistGitAPI) { + $info = $this->getGitParentLogInfo(); + if ($info['parent']) { + $parent = $info['parent']; + } + if ($info['base_revision']) { + $base_revision = $info['base_revision']; + } + if ($info['base_path']) { + $base_path = $info['base_path']; + } + } + + $paths = $this->generateAffectedPaths(); + + $lint_result = $this->runLint($paths); + $unit_result = $this->runUnit($paths); + + $changes = $this->generateChanges(); + if (!$changes) { + throw new ArcanistUsageException( + "There are no changes to generate a diff from!"); + } + + $change_list = array(); + foreach ($changes as $change) { + $change_list[] = $change->toDictionary(); + } + + if ($lint_result === ArcanistLintWorkflow::RESULT_OKAY) { + $lint = 'okay'; + } else if ($lint_result === ArcanistLintWorkflow::RESULT_WARNINGS) { + $lint = 'fail'; + } else if ($lint_result === ArcanistLintWorkflow::RESULT_SKIP) { + $lint = 'skip'; + } else { + $lint = 'none'; + } + + if ($unit_result === ArcanistUnitWorkflow::RESULT_OKAY) { + $unit = 'okay'; + } else if ($unit_result === ArcanistUnitWorkflow::RESULT_UNSOUND || + $unit_result === ArcanistUnitWorkflow::RESULT_FAIL) { + $unit = 'fail'; + } else if ($unit_result === ArcanistUnitWorkflow::RESULT_SKIP) { + $unit = 'skip'; + } else { + $unit = 'none'; + } + + $diff = array( + 'changes' => $change_list, + 'sourceMachine' => php_uname('n'), + 'sourcePath' => $repository_api->getPath(), + 'branch' => $repository_api->getBranchName(), + 'sourceControlSystem' => + $repository_api->getSourceControlSystemName(), + 'sourceControlPath' => $base_path, + 'sourceControlBaseRevision' => $base_revision, + 'parentRevisionID' => $parent, + 'lintStatus' => $lint, + 'unitStatus' => $unit, + ); + + $diff_info = $conduit->callMethodSynchronous( + 'differential.creatediff', + $diff); + + if ($this->shouldOnlyCreateDiff()) { + echo phutil_console_format( + "Created a new Differential diff:\n". + " **Diff URI:** __%s__\n\n", + $diff_info['uri']); + } else { + $message = $this->getGitCommitMessage(); + + $revision = array( + 'diffid' => $diff_info['diffid'], + 'fields' => $message->getFields(), + ); + + if ($message->getRevisionID()) { + + $update_message = $this->getUpdateMessage(); + + $revision['id'] = $message->getRevisionID(); + $revision['message'] = $update_message; + $future = $conduit->callMethod( + 'differential.updaterevision', + $revision); + $result = $future->resolve(); + echo "Updated an existing Differential revision:\n"; + } else { + $revision['user'] = $this->getUserGUID(); + $future = $conduit->callMethod( + 'differential.createrevision', + $revision); + $result = $future->resolve(); + echo "Updating commit message to include Differential revision ID...\n"; + $repository_api->amendGitHeadCommit( + $message->getRawCorpus(). + "\n\n". + "Differential Revision: ".$result['revisionid']."\n"); + echo "Created a new Differential revision:\n"; + } + + $uri = $result['uri']; + echo phutil_console_format( + " **Revision URI:** __%s__\n\n", + $uri); + } + + echo "Included changes:\n"; + foreach ($changes as $change) { + echo ' '.$change->renderTextSummary()."\n"; + } + + return 0; + } + + protected function shouldOnlyCreateDiff() { + $repository_api = $this->getRepositoryAPI(); + if ($repository_api instanceof ArcanistSubversionAPI) { + return true; + } + return $this->getArgument('preview') || + $this->getArgument('only'); + } + + protected function findRevisionInformation() { + return array(null, null); + } + + private function generateAffectedPaths() { + $repository_api = $this->getRepositoryAPI(); + if ($repository_api instanceof ArcanistSubversionAPI) { + $file_list = new FileList($this->getArgument('paths', array())); + $paths = $repository_api->getSVNStatus($externals = true); + foreach ($paths as $path => $mask) { + if (!$file_list->contains($repository_api->getPath($path), true)) { + unset($paths[$path]); + } + } + + $warn_externals = array(); + foreach ($paths as $path => $mask) { + $any_mod = ($mask & ArcanistRepositoryAPI::FLAG_ADDED) || + ($mask & ArcanistRepositoryAPI::FLAG_MODIFIED) || + ($mask & ArcanistRepositoryAPI::FLAG_DELETED); + if ($mask & ArcanistRepositoryAPI::FLAG_EXTERNALS) { + unset($paths[$path]); + if ($any_mod) { + $warn_externals[] = $path; + } + } + } + + if ($warn_externals && !$this->hasWarnedExternals) { + echo phutil_console_format( + "The working copy includes changes to 'svn:externals' paths. These ". + "changes will not be included in the diff because SVN can not ". + "commit 'svn:externals' changes alongside normal changes.". + "\n\n". + "Modified 'svn:externals' files:". + "\n\n". + ' '.phutil_console_wrap(implode("\n", $warn_externals), 8)); + $prompt = "Generate a diff (with just local changes) anyway?"; + if (!phutil_console_confirm($prompt)) { + throw new ArcanistUserAbortException(); + } else { + $this->hasWarnedExternals = true; + } + } + + } else { + $this->parseGitRelativeCommit( + $repository_api, + $this->getArgument('paths', array())); + $paths = $repository_api->getWorkingCopyStatus(); + } + return $paths; + } + + + protected function generateChanges() { + $repository_api = $this->getRepositoryAPI(); + + $parser = new ArcanistDiffParser(); + if ($repository_api instanceof ArcanistSubversionAPI) { + $paths = $this->generateAffectedPaths(); + $this->primeSubversionWorkingCopyData($paths); + + // Check to make sure the user is diffing from a consistent base revision. + // This is mostly just an abuse sanity check because it's silly to do this + // and makes the code more difficult to effectively review, but it also + // affects patches and makes them nonportable. + $bases = $repository_api->getSVNBaseRevisions(); + + // Remove all files with baserev "0"; these files are new. + foreach ($bases as $path => $baserev) { + if ($bases[$path] == 0) { + unset($bases[$path]); + } + } + + if ($bases) { + // We have at least one path which isn't new. + $repository_info = $repository_api->getSVNInfo('/'); + $bases['.'] = $repository_info['Revision']; + if ($bases['.']) { + $rev = $bases['.']; + foreach ($bases as $path => $baserev) { + if ($baserev !== $rev) { + $revlist = array(); + foreach ($bases as $path => $baserev) { + $revlist[] = " Revision {$baserev}, {$path}"; + } + $revlist = implode("\n", $revlist); + throw new ArcanistUsageException( + "Base revisions of changed paths are mismatched. Update all ". + "paths to the same base revision before creating a diff: ". + "\n\n". + $revlist); + } + } + } + } + + $changes = $parser->parseSubversionDiff( + $repository_api, + $paths); + } else if ($repository_api instanceof ArcanistGitAPI) { + + $diff = $repository_api->getFullGitDiff(); + if (!strlen($diff)) { + list($base, $tip) = $repository_api->getCommitRange(); + if ($tip == 'HEAD') { + if (preg_match('/\^+HEAD/', $base)) { + $more = 'Did you mean HEAD^ instead of ^HEAD?'; + } else { + $more = 'Did you specify the wrong relative commit?'; + } + } else { + $more = 'Did you specify the wrong commit range?'; + } + throw new ArcanistUsageException("No changes found. ({$more})"); + } + $changes = $parser->parseDiff($diff); + + } else { + throw new Exception("Repository API is not supported."); + } + + if (count($changes) > 250) { + $count = number_format(count($changes)); + $message = + "This diff has a very large number of changes ({$count}). ". + "Differential works best for changes which will receive detailed ". + "human review, and not as well for large automated changes or ". + "bulk checkins. Continue anyway?"; + if (!phutil_console_confirm($message)) { + throw new ArcanistUsageException( + "Aborted generation of gigantic diff."); + } + } + + $limit = 1024 * 1024 * 4; + foreach ($changes as $change) { + $size = 0; + foreach ($change->getHunks() as $hunk) { + $size += strlen($hunk->getCorpus()); + } + if ($size > $limit) { + $file_name = $change->getCurrentPath(); + $change_size = number_format($size); + $byte_warning = + "Diff for '{$file_name}' with context is {$change_size} bytes in ". + "length. Generally, source changes should not be this large. If ". + "this file is a huge text file, try using the '--less-context' flag."; + if ($repository_api instanceof ArcanistSubversionAPI) { + throw new ArcanistUsageException( + "{$byte_warning} If the file is not a text file, mark it as ". + "binary with:". + "\n\n". + " $ svn propset svn:mime-type application/octet-stream ". + "\n"); + } else { + $confirm = + "{$byte_warning} If the file is not a text file, you can ". + "mark it 'binary'. Mark this file as 'binary' and continue?"; + if (phutil_console_confirm($confirm)) { + $change->convertToBinaryChange(); + } else { + throw new ArcanistUsageException( + "Aborted generation of gigantic diff."); + } + } + } + } + + return $changes; + } + + /** + * Retrieve the git message in HEAD if it isn't a primary template message. + */ + private function getGitUpdateMessage() { + $repository_api = $this->getRepositoryAPI(); + + $parser = new ArcanistDiffParser($repository_api); + $commit_messages = $repository_api->getGitCommitLog(); + $commit_messages = $parser->parseDiff($commit_messages); + + $head = reset($commit_messages); + $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( + $head->getMetadata('message')); + if ($message->getRevisionID()) { + return null; + } + + return trim($message->getRawCorpus()); + } + + private function getGitCommitMessage() { + $conduit = $this->getConduit(); + $repository_api = $this->getRepositoryAPI(); + + $parser = new ArcanistDiffParser($repository_api); + $commit_messages = $repository_api->getGitCommitLog(); + $commit_messages = $parser->parseDiff($commit_messages); + + $problems = array(); + $parsed = array(); + foreach ($commit_messages as $key => $change) { + $problems[$key] = array(); + + try { + $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( + $change->getMetadata('message')); + + $message->pullDataFromConduit($conduit); + + $parsed[$key] = $message; + } catch (ArcanistDifferentialCommitMessageParserException $ex) { + $problems[$key][] = $ex; + continue; + } + + // TODO: Move this all behind Conduit. + if (!$message->getRevisionID()) { + if ($message->getFieldValue('reviewedByGUIDs')) { + $problems[$key][] = new ArcanistUsageException( + "When creating or updating a revision, use the 'Reviewers:' ". + "field to specify reviewers, not 'Reviewed By:'. After the ". + "revision is accepted, run 'arc amend' to update the commit ". + "message."); + } + + if (!$message->getFieldValue('title')) { + $problems[$key][] = new ArcanistUsageException( + "Commit message has no title. You must provide a title for this ". + "revision."); + } + + if (!$message->getFieldValue('testPlan')) { + $problems[$key][] = new ArcanistUsageException( + "Commit message has no 'Test Plan:'. You must provide a test ". + "plan."); + } + } + } + + $blessed = null; + $revision_id = -1; + foreach ($problems as $key => $problem_list) { + if ($problem_list) { + continue; + } + if ($revision_id === -1) { + $revision_id = $parsed[$key]->getRevisionID(); + $blessed = $parsed[$key]; + } else { + throw new ArcanistUsageException( + "Changes in the specified commit range include more than one ". + "commit with a valid template commit message. This is ambiguous, ". + "your commit range should contain only one template commit ". + "message. Alternatively, use --diff-only to ignore commit ". + "messages."); + } + } + + if ($revision_id === -1) { + $all_problems = call_user_func_array('array_merge', $problems); + $desc = implode("\n", mpull($all_problems, 'getMessage')); + if (count($problems) > 1) { + throw new ArcanistUsageException( + "All changes between the specified commits have template parsing ". + "problems:\n\n".$desc."\n\nIf you only want to create a diff ". + "(not a revision), use --diff-only to ignore commit messages."); + } else if (count($problems) == 1) { + throw new ArcanistUsageException( + "Commit message is not properly formatted:\n\n".$desc."\n\n". + "You should use the standard git commit template to provide a ". + "commit message. If you only want to create a diff (not a ". + "revision), use --diff-only to ignore commit messages."); + } + } + + if ($blessed) { + if (!$blessed->getFieldValue('reviewerGUIDs')) { + $message = "You have not specified any reviewers. Continue anyway?"; + if (!phutil_console_confirm($message)) { + throw new ArcanistUsageException('Specify reviewers and retry.'); + } + } + } + + return $blessed; + } + + private function getGitParentLogInfo() { + $info = array( + 'parent' => null, + 'base_revision' => null, + 'base_path' => null, + ); + + $conduit = $this->getConduit(); + $repository_api = $this->getRepositoryAPI(); + + $parser = new ArcanistDiffParser($repository_api); + $history_messages = $repository_api->getGitHistoryLog(); + if (!$history_messages) { + // This can occur on the initial commit. + return $info; + } + $history_messages = $parser->parseDiff($history_messages); + + foreach ($history_messages as $key => $change) { + try { + $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( + $change->getMetadata('message')); + if ($message->getRevisionID() && $info['parent'] === null) { + $info['parent'] = $message->getRevisionID(); + } + if ($message->getGitSVNBaseRevision() && + $info['base_revision'] === null) { + $info['base_revision'] = $message->getGitSVNBaseRevision(); + $info['base_path'] = $message->getGitSVNBasePath(); + } + if ($info['parent'] && $info['base_revision']) { + break; + } + } catch (ArcanistDifferentialCommitMessageParserException $ex) { + // Ignore. + } + } + + return $info; + } + + protected function primeSubversionWorkingCopyData($paths) { + $repository_api = $this->getRepositoryAPI(); + + $futures = array(); + $targets = array(); + foreach ($paths as $path => $mask) { + $futures[] = $repository_api->buildDiffFuture($path); + $targets[] = array('command' => 'diff', 'path' => $path); + $futures[] = $repository_api->buildInfoFuture($path); + $targets[] = array('command' => 'info', 'path' => $path); + } + + foreach ($futures as $key => $future) { + $target = $targets[$key]; + if ($target['command'] == 'diff') { + $repository_api->primeSVNDiffResult( + $target['path'], + $future->resolve()); + } else { + $repository_api->primeSVNInfoResult( + $target['path'], + $future->resolve()); + } + } + } + + private function getUpdateMessage() { + $comments = $this->getArgument('message'); + if (!strlen($comments)) { + + // When updating a revision using git without specifying '--message', try + // to prefill with the message in HEAD if it isn't a template message. The + // idea is that if you do: + // + // $ git commit -a -m 'fix some junk' + // $ arc diff + // + // ...you shouldn't have to retype the update message. + $repository_api = $this->getRepositoryAPI(); + if ($repository_api instanceof ArcanistGitAPI) { + $comments = $this->getGitUpdateMessage(); + } + + $template = + $comments. + "\n\n". + "# Enter a brief description of the changes included in this update.". + "\n"; + $comments = id(new PhutilInteractiveEditor($template)) + ->setName('differential-update-comments') + ->editInteractively(); + $comments = preg_replace('/^\s*#.*$/m', '', $comments); + } + + return $comments; + } + + private function runLint($paths) { + if ($this->getArgument('nolint') || + $this->getArgument('only')) { + return ArcanistLintWorkflow::RESULT_SKIP; + } + + $repository_api = $this->getRepositoryAPI(); + + echo "Linting...\n"; + try { + $argv = array(); + if ($this->getArgument('lintall')) { + $argv[] = '--lintall'; + } + if ($this->getArgument('advice')) { + $argv[] = '--advice'; + } + if ($repository_api instanceof ArcanistSubversionAPI) { + $argv = array_merge($argv, array_keys($paths)); + } else { + $argv[] = $repository_api->getRelativeCommit(); + } + $lint_workflow = $this->buildChildWorkflow('lint', $argv); + $lint_result = $lint_workflow->run(); + + switch ($lint_result) { + case ArcanistLintWorkflow::RESULT_OKAY: + echo phutil_console_format( + "** LINT OKAY ** No lint problems.\n"); + break; + case ArcanistLintWorkflow::RESULT_WARNINGS: + $continue = phutil_console_confirm( + "Lint issued unresolved warnings. Ignore them?"); + if (!$continue) { + throw new ArcanistUserAbortException(); + } + break; + case ArcanistLintWorkflow::RESULT_ERRORS: + echo phutil_console_format( + "** LINT ERRORS ** Lint raised errors!\n"); + throw new ArcanistUsageException( + "Resolve lint errors or run with --nolint."); + break; + } + + return $lint_result; + } catch (ArcanistNoEngineException $ex) { + echo "No lint engine configured for this project.\n"; + } catch (ArcanistNoEffectException $ex) { + echo "No paths to lint.\n"; + } + + return null; + } + + private function runUnit($paths) { + if ($this->getArgument('nounit') || + $this->getArgument('only')) { + return ArcanistUnitWorkflow::RESULT_SKIP; + } + + $repository_api = $this->getRepositoryAPI(); + + echo "Running unit tests...\n"; + try { + $argv = array(); + if ($repository_api instanceof ArcanistSubversionAPI) { + $argv = array_merge($argv, array_keys($paths)); + } + $unit_workflow = $this->buildChildWorkflow('unit', $argv); + $unit_result = $unit_workflow->run(); + switch ($unit_result) { + case ArcanistUnitWorkflow::RESULT_OKAY: + echo phutil_console_format( + "** UNIT OKAY ** No unit test failures.\n"); + break; + case ArcanistUnitWorkflow::RESULT_UNSOUND: + $continue = phutil_console_confirm( + "Unit test results included failures, but all failing tests ". + "are known to be unsound. Ignore unsound test failures?"); + if (!$continue) { + throw new ArcanistUserAbortException(); + } + break; + case ArcanistUnitWorkflow::RESULT_FAIL: + echo phutil_console_format( + "** UNIT ERRORS ** Unit testing raised errors!\n"); + throw new ArcanistUsageException( + "Resolve unit test errors or run with --nounit."); + break; + } + + return $unit_result; + } catch (ArcanistNoEngineException $ex) { + echo "No unit test engine is configured for this project.\n"; + } catch (ArcanistNoEffectException $ex) { + echo "No tests to run.\n"; + } + + return null; + } + +} diff --git a/src/workflow/diff/__init__.php b/src/workflow/diff/__init__.php new file mode 100644 index 00000000..5a60aac7 --- /dev/null +++ b/src/workflow/diff/__init__.php @@ -0,0 +1,24 @@ + array( + 'help' => + "Export change as a git patch. This format is more complete than ". + "unified, but less complete than arc bundles. These patches can be ". + "applied with 'git apply' or 'arc patch'.", + ), + 'unified' => array( + 'help' => + "Export change as a unified patch. This format is less complete ". + "than git patches or arc bundles. These patches can be applied with ". + "'patch' or 'arc patch'.", + ), + 'arcbundle' => array( + 'param' => 'file', + 'help' => + "Export change as an arc bundle. This format can represent all ". + "changes. These bundles can be applied with 'arc patch'.", + ), + 'revision' => array( + 'param' => 'revision_id', + 'help' => + "Instead of exporting changes from the working copy, export them ". + "from a Differential revision." + ), + 'diff' => array( + 'param' => 'diff_id', + 'help' => + "Instead of exporting changes from the working copy, export them ". + "from a Differential diff." + ), + '*' => 'paths', + ); + } + + + protected function didParseArguments() { + $source = self::SOURCE_LOCAL; + $requested = 0; + if ($this->getArgument('revision')) { + $source = self::SOURCE_REVISION; + $requested++; + } + if ($this->getArgument('diff')) { + $source = self::SOURCE_DIFF; + $requested++; + } + + if ($requested > 1) { + throw new ArcanistUsageException( + "Options '--revision' and '--diff' are not compatible. Choose exactly ". + "one change source."); + } + + $this->source = $source; + $this->sourceID = $this->getArgument($source); + + $format = null; + $requested = 0; + if ($this->getArgument('git')) { + $format = self::FORMAT_GIT; + $requested++; + } + if ($this->getArgument('unified')) { + $format = self::FORMAT_UNIFIED; + $requested++; + } + if ($this->getArgument('arcbundle')) { + $format = self::FORMAT_BUNDLE; + $requested++; + } + + if ($requested === 0) { + throw new ArcanistUsageException( + "Specify one of '--git', '--unified' or '--arcbundle ' to ". + "choose an export format."); + } else if ($requested > 1) { + throw new ArcanistUsageException( + "Options '--git', '--unified' and '--arcbundle' are not compatible. ". + "Choose exactly one export format."); + } + + $this->format = $format; + } + + public function requiresConduit() { + return $this->getSource() != self::SOURCE_LOCAL; + } + + public function requiresAuthentication() { + return $this->requiresConduit(); + } + + public function requiresRepositoryAPI() { + return $this->getSource() == self::SOURCE_LOCAL; + } + + public function requiresWorkingCopy() { + return $this->getSource() == self::SOURCE_LOCAL; + } + + private function getSource() { + return $this->source; + } + + private function getSourceID() { + return $this->sourceID; + } + + private function getFormat() { + return $this->format; + } + + public function run() { + + $source = $this->getSource(); + + switch ($source) { + case self::SOURCE_LOCAL: + $repository_api = $this->getRepositoryAPI(); + $parser = new ArcanistDiffParser(); + + // TODO: git support, paths support + $paths = $repository_api->getWorkingCopyStatus(); + $changes = $parser->parseSubversionDiff( + $repository_api, + $paths); + + $bundle = ArcanistBundle::newFromChanges($changes); + break; + case self::SOURCE_REVISION: + $bundle = $this->loadRevisionBundleFromConduit( + $this->getConduit(), + $this->getSourceID()); + break; + case self::SOURCE_DIFF: + $bundle = $this->loadDiffBundleFromConduit( + $this->getConduit(), + $this->getSourceID()); + break; + } + + $format = $this->getFormat(); + + switch ($format) { + case self::FORMAT_GIT: + echo $bundle->toGitPatch(); + break; + case self::FORMAT_UNIFIED: + echo $bundle->toUnifiedDiff(); + break; + case self::FORMAT_BUNDLE: + $path = $this->getArgument('arcbundle'); + echo "Writing bundle to '{$path}'... "; + $bundle->writeToDisk($path); + echo "done.\n"; + break; + } + + return 0; + } +} diff --git a/src/workflow/export/__init__.php b/src/workflow/export/__init__.php new file mode 100644 index 00000000..7a729db8 --- /dev/null +++ b/src/workflow/export/__init__.php @@ -0,0 +1,17 @@ +getWorkingCopy(); + if (!$working_copy->getProjectID()) { + throw new ArcanistUsageException( + "You have installed a git pre-receive hook in a remote without an ". + ".arcconfig."); + } + + if (!$working_copy->getConfig('remote_hooks_installed')) { + echo phutil_console_wrap( + "\n". + "NOTE: Arcanist is installed as a git pre-receive hook in the git ". + "remote you are pushing to, but the project's '.arcconfig' does not ". + "have the 'remote_hooks_installed' flag set. Until you set the flag, ". + "some code will run needlessly in both the local and remote, and ". + "revisions will be marked 'committed' in Differential when they are ". + "amended rather than when they are actually pushed to the remote ". + "origin.". + "\n\n"); + } + + // Git repositories have special rules in pre-receive hooks. We need to + // construct the API against the .git directory instead of the project + // root or commands don't work properly. + $repository_api = ArcanistGitAPI::newHookAPI($_SERVER['PWD']); + + $root = $working_copy->getProjectRoot(); + + $parser = new ArcanistDiffParser(); + + $mark_revisions = array(); + + $stdin = file_get_contents('php://stdin'); + $commits = array_filter(explode("\n", $stdin)); + foreach ($commits as $commit) { + list($old_ref, $new_ref, $refname) = explode(' ', $commit); + + list($log) = execx( + '(cd %s && git log -n1 %s)', + $repository_api->getPath(), + $new_ref); + $message_log = reset($parser->parseDiff($log)); + $message = ArcanistDifferentialCommitMessage::newFromRawCorpus( + $message_log->getMetadata('message')); + + $revision_id = $message->getRevisionID(); + if ($revision_id) { + $mark_revisions[] = $revision_id; + } + + // TODO: Do commit message junk. + + $info = $repository_api->getPreReceiveHookStatus($old_ref, $new_ref); + $paths = ipull($info, 'mask'); + $frefs = ipull($info, 'ref'); + $data = array(); + foreach ($paths as $path => $mask) { + list($stdout) = execx( + '(cd %s && git cat-file blob %s)', + $repository_api->getPath(), + $frefs[$path]); + $data[$path] = $stdout; + } + + // TODO: Do commit content junk. + + $commit_name = $new_ref; + if ($revision_id) { + $commit_name = 'D'.$revision_id.' ('.$commit_name.')'; + } + + echo "[arc pre-receive] {$commit_name} OK...\n"; + } + + $conduit = $this->getConduit(); + + $futures = array(); + foreach ($mark_revisions as $revision_id) { + $futures[] = $conduit->callMethod( + 'differential.markcommitted', + array( + 'revision_id' => $revision_id, + )); + } + + Futures($futures)->resolveAll(); + + return 0; + } +} diff --git a/src/workflow/git-hook-pre-receive/__init__.php b/src/workflow/git-hook-pre-receive/__init__.php new file mode 100644 index 00000000..b931102f --- /dev/null +++ b/src/workflow/git-hook-pre-receive/__init__.php @@ -0,0 +1,21 @@ + 'command', + ); + } + + public function run() { + + $arc_config = $this->getArcanistConfiguration(); + $workflows = $arc_config->buildAllWorkflows(); + ksort($workflows); + + $target = null; + if ($this->getArgument('command')) { + $target = reset($this->getArgument('command')); + if (empty($workflows[$target])) { + throw new ArcanistUsageException( + "Unrecognized command '{$target}'. Try 'arc help'."); + } + } + + $cmdref = array(); + foreach ($workflows as $command => $workflow) { + if ($target && $target != $command) { + continue; + } + $optref = array(); + $arguments = $workflow->getArguments(); + + $config_arguments = $arc_config->getCustomArgumentsForCommand($command); + + // This juggling is to put the extension arguments after the normal + // arguments, and make sure the normal arguments aren't overwritten. + ksort($arguments); + ksort($config_arguments); + foreach ($config_arguments as $argument => $spec) { + if (empty($arguments[$argument])) { + $arguments[$argument] = $spec; + } + } + + foreach ($arguments as $argument => $spec) { + if ($argument == '*') { + continue; + } + if (isset($spec['param'])) { + if (isset($spec['short'])) { + $optref[] = phutil_console_format( + " __--%s__ __%s__, __-%s__ __%s__", + $argument, + $spec['param'], + $spec['short'], + $spec['param']); + } else { + $optref[] = phutil_console_format( + " __--%s__ __%s__", + $argument, + $spec['param']); + } + } else { + if (isset($spec['short'])) { + $optref[] = phutil_console_format( + " __--%s__, __-%s__", + $argument, + $spec['short']); + } else { + $optref[] = phutil_console_format( + " __--%s__", + $argument); + } + } + + if (isset($config_arguments[$argument])) { + $optref[] = " (This is a custom option for this ". + "project.)"; + } + + if (isset($spec['supports'])) { + $optref[] = " Supports: ". + implode(', ', $spec['supports']); + } + + if (isset($spec['help'])) { + $docs = $spec['help']; + } else { + $docs = 'This option is not documented.'; + } + $docs = phutil_console_wrap($docs, 14); + $optref[] = " {$docs}\n"; + } + if ($optref) { + $optref = implode("\n", $optref); + $optref = "\n\n".$optref; + } else { + $optref = "\n"; + } + + $cmdref[] = $workflow->getCommandHelp().$optref; + } + $cmdref = implode("\n\n", $cmdref); + + if ($target) { + echo "\n".$cmdref."\n"; + return; + } + + $self = 'arc'; + echo phutil_console_format(<< array( + 'help' => + "Show all lint warnings, not just those on changed lines." + ), + 'summary' => array( + 'help' => + "Show lint warnings in a more compact format." + ), + 'advice' => array( + 'help' => + "Show lint advice, not just warnings and errors." + ), + 'engine' => array( + 'param' => 'classname', + 'help' => + "Override configured lint engine for this project." + ), + '*' => 'paths', + ); + } + + public function requiresWorkingCopy() { + return true; + } + + public function run() { + $working_copy = $this->getWorkingCopy(); + + $engine = $this->getArgument('engine'); + if (!$engine) { + $engine = $working_copy->getConfig('lint_engine'); + } + + $should_lint_all = $this->getArgument('lintall'); + + $repository_api = null; + if (!$should_lint_all) { + try { + $repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity( + $working_copy); + $this->setRepositoryAPI($repository_api); + } catch (ArcanistUsageException $ex) { + throw new ArcanistUsageException( + $ex->getMessage()."\n\n". + "Use '--lintall' to ignore working copy changes when running lint."); + } + + if ($repository_api instanceof ArcanistSubversionAPI) { + $paths = $repository_api->getWorkingCopyStatus(); + $list = new FileList($this->getArgument('paths')); + foreach ($paths as $path => $flags) { + if (!$list->contains($path)) { + unset($paths[$path]); + } + } + } else { + $this->parseGitRelativeCommit( + $repository_api, + $this->getArgument('paths')); + $paths = $repository_api->getWorkingCopyStatus(); + } + + foreach ($paths as $path => $flags) { + if ($flags & ArcanistRepositoryAPI::FLAG_UNTRACKED) { + unset($paths[$path]); + } + } + + $paths = array_keys($paths); + + } else { + $paths = $this->getArgument('paths'); + if (empty($paths)) { + throw new ArcanistUsageException( + "You must specify one or more files to lint when using '--lintall'."); + } + } + + if (!$engine) { + throw new ArcanistNoEngineException( + "No lint engine configured for this project. Edit .arcconfig to ". + "specify a lint engine."); + } + + $ok = phutil_autoload_class($engine); + if (!$ok) { + throw new ArcanistUsageException( + "Configured lint engine '{$engine}' could not be loaded."); + } + + $engine = newv($engine, array()); + $engine->setWorkingCopy($working_copy); + + if ($this->getArgument('advice')) { + $engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ADVICE); + } else { + $engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_WARNING); + } + + $engine->setPaths($paths); + if (!$should_lint_all) { + foreach ($paths as $path) { + $engine->setPathChangedLines( + $path, + $this->getChangedLines($path, 'new')); + } + } + + $results = $engine->run(); + + $apply_patches = true; + $prompt_patches = true; + $wrote_to_disk = false; + + $renderer = new ArcanistLintRenderer(); + if ($this->getArgument('summary')) { + $renderer->setSummaryMode(true); + } + foreach ($results as $result) { + if (!$result->getMessages()) { + continue; + } + + echo $renderer->renderLintResult($result); + + if ($apply_patches && $result->isPatchable()) { + $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result); + $old = $patcher->getUnmodifiedFileContent(); + $new = $patcher->getModifiedFileContent(); + + if ($prompt_patches) { + $old_file = $result->getFilePathOnDisk(); + $new_file = new TempFile(); + Filesystem::writeFile($new_file, $new); + + // TODO: Improve the behavior here, make it more like + // difference_render(). + passthru(csprintf("diff -u %s %s", $old_file, $new_file)); + + $prompt = phutil_console_format( + "Apply this patch to __%s__?", + $result->getPath()); + if (!phutil_console_confirm($prompt, $default_no = false)) { + continue; + } + } + + $patcher->writePatchToDisk(); + $wrote_to_disk = true; + } + } + + if ($wrote_to_disk && ($repository_api instanceof ArcanistGitAPI)) { + $amend = phutil_console_confirm( + "Amend HEAD with lint patches?", + $default_no = false); + if (!$amend) { + throw new ArcanistUsageException("Resolve lint changes and rediff."); + } + execx( + '(cd %s; git commit -a --amend -C HEAD)', + $repository_api->getPath()); + } + + $result_code = self::RESULT_OKAY; + foreach ($results as $result) { + foreach ($result->getMessages() as $message) { + if (!$message->isPatchApplied()) { + if ($message->isError()) { + $result_code = self::RESULT_ERRORS; + break; + } else if ($message->isWarning()) { + $result_code = self::RESULT_WARNINGS; + } + } + } + } + + if (!$this->getParentWorkflow()) { + if ($result_code == self::RESULT_OKAY) { + echo phutil_console_format( + "** OKAY ** No lint warnings.\n"); + } + } + + return $result_code; + } +} diff --git a/src/workflow/lint/__init__.php b/src/workflow/lint/__init__.php new file mode 100644 index 00000000..9f6a024a --- /dev/null +++ b/src/workflow/lint/__init__.php @@ -0,0 +1,27 @@ +getConduit(); + $repository_api = $this->getRepositoryAPI(); + + $revision_future = $conduit->callMethod( + 'differential.find', + array( + 'guids' => array($this->getUserGUID()), + 'query' => 'open', + )); + + $revisions = array(); + foreach ($revision_future->resolve() as $revision_dict) { + $revisions[] = ArcanistDifferentialRevisionRef::newFromDictionary( + $revision_dict); + } + + if (!$revisions) { + echo "You have no open Differential revisions.\n"; + return 0; + } + + + foreach ($revisions as $revision) { + $revision_path = Filesystem::resolvePath($revision->getSourcePath()); + $current_path = Filesystem::resolvePath($repository_api->getPath()); + $from_here = ($revision_path == $current_path); + + printf( + " %15s | %s | D%d | %s\n", + $revision->getStatusName(), + $from_here ? '*' : ' ', + $revision->getID(), + $revision->getName()); + } + + return 0; + } +} diff --git a/src/workflow/list/__init__.php b/src/workflow/list/__init__.php new file mode 100644 index 00000000..06d6373b --- /dev/null +++ b/src/workflow/list/__init__.php @@ -0,0 +1,16 @@ + 'revision', + ); + } + + public function requiresConduit() { + return true; + } + + public function requiresAuthentication() { + return true; + } + + public function run() { + + $conduit = $this->getConduit(); + + $revision_list = $this->getArgument('revision', array()); + if (!$revision_list) { + throw new ArcanistUsageException( + "mark-committed requires a revision number."); + } + if (count($revision_list) != 1) { + throw new ArcanistUsageException( + "mark-committed requires exactly one revision."); + } + + $revision_data = $conduit->callMethodSynchronous( + 'differential.find', + array( + 'query' => 'committable', + 'guids' => array( + $this->getUserGUID(), + ), + )); + + try { + $revision_id = reset($revision_list); + $revision_id = $this->normalizeRevisionID($revision_id); + $revision = $this->chooseRevision( + $revision_data, + $revision_id); + } catch (ArcanistChooseInvalidRevisionException $ex) { + throw new ArcanistUsageException( + "Revision D{$revision_id} is not committable. You can only mark ". + "revisions which have been 'accepted' as committed."); + } + + $revision_id = $revision->getID(); + $revision_name = $revision->getName(); + + echo "Marking revision D{$revision_id} '{$revision_name}' committed...\n"; + + $conduit->callMethodSynchronous( + 'differential.markcommitted', + array( + 'revision_id' => $revision_id, + )); + + echo "Done.\n"; + + return 0; + } +} diff --git a/src/workflow/mark-committed/__init__.php b/src/workflow/mark-committed/__init__.php new file mode 100644 index 00000000..83bdcd8d --- /dev/null +++ b/src/workflow/mark-committed/__init__.php @@ -0,0 +1,15 @@ + array( + 'param' => 'revision_id', + 'help' => + "Apply changes from a Differential revision, using the most recent ". + "diff that has been attached to it.", + ), + 'diff' => array( + 'param' => 'diff_id', + 'help' => + "Apply changes from a Differential diff. Normally you want to use ". + "--revision to get the most recent changes, but you can ". + "specifically apply an out-of-date diff or a diff which was never ". + "attached to a revision by using this flag.", + ), + 'arcbundle' => array( + 'param' => 'bundlefile', + 'help' => + "Apply changes from an arc bundle generated with 'arc export'.", + ), + 'patch' => array( + 'param' => 'patchfile', + 'help' => + "Apply changes from a git patchfile or unified patchfile.", + ), + ); + } + + protected function didParseArguments() { + $source = null; + $requested = 0; + if ($this->getArgument('revision')) { + $source = self::SOURCE_REVISION; + $requested++; + } + if ($this->getArgument('diff')) { + $source = self::SOURCE_DIFF; + $requested++; + } + if ($this->getArgument('arcbundle')) { + $source = self::SOURCE_BUNDLE; + $requested++; + } + if ($this->getArgument('patch')) { + $source = self::SOURCE_PATCH; + $requested++; + } + + if ($requested === 0) { + throw new ArcanistUsageException( + "Specify one of '--revision ' (to select the current ". + "changes attached to a Differential revision), '--diff ' ". + "(to select a specific, out-of-date diff or a diff which is not ". + "attached to a revision), '--arcbundle ' or '--patch ' ". + "to choose a patch source."); + } else if ($requested > 1) { + throw new ArcanistUsageException( + "Options '--revision', '--diff', '--arcbundle' and '--patch' are ". + "not compatible. Choose exactly one patch source."); + } + + $this->source = $source; + $this->sourceParam = $this->getArgument($source); + } + + public function requiresConduit() { + return ($this->getSource() == self::SOURCE_REVISION) || + ($this->getSource() == self::SOURCE_DIFF); + } + + public function requiresAuthentication() { + return $this->requiresConduit(); + } + + public function requiresRepositoryAPI() { + return true; + } + + public function requiresWorkingCopy() { + return true; + } + + private function getSource() { + return $this->source; + } + + private function getSourceParam() { + return $this->sourceParam; + } + + public function run() { + + $source = $this->getSource(); + $param = $this->getSourceParam(); + switch ($source) { + case self::SOURCE_PATCH: + if ($param == '-') { + $patch = @file_get_contents('php://stdin'); + if (!strlen($patch)) { + throw new ArcanistUsageException( + "Failed to read patch from stdin!"); + } + } else { + $patch = Filesystem::readFile($param); + } + $bundle = ArcanistBundle::newFromDiff($patch); + break; + case self::SOURCE_BUNDLE: + $path = $this->getArgument('arcbundle'); + $bundle = ArcanistBundle::newFromArcBundle($path); + break; + case self::SOURCE_REVISION: + $bundle = $this->loadRevisionBundleFromConduit( + $this->getConduit(), + $param); + break; + case self::SOURCE_DIFF: + $bundle = $this->loadDiffBundleFromConduit( + $this->getConduit(), + $param); + break; + } + + $repository_api = $this->getRepositoryAPI(); + if ($repository_api instanceof ArcanistSubversionAPI) { + $copies = array(); + $deletes = array(); + $patches = array(); + $propset = array(); + $adds = array(); + + $changes = $bundle->getChanges(); + foreach ($changes as $change) { + $type = $change->getType(); + $should_patch = true; + switch ($type) { + case ArcanistDiffChangeType::TYPE_MOVE_AWAY: + case ArcanistDiffChangeType::TYPE_MULTICOPY: + case ArcanistDiffChangeType::TYPE_DELETE: + $path = $change->getCurrentPath(); + $fpath = $repository_api->getPath($path); + if (!@file_exists($fpath)) { + $this->confirm( + "Patch deletes file '{$path}', but the file does not exist in ". + "the working copy. Continue anyway?"); + } else { + $deletes[] = $change->getCurrentPath(); + } + $should_patch = false; + break; + case ArcanistDiffChangeType::TYPE_COPY_HERE: + case ArcanistDiffChangeType::TYPE_MOVE_HERE: + $path = $change->getOldPath(); + $fpath = $repository_api->getPath($path); + if (!@file_exists($fpath)) { + $cpath = $change->getCurrentPath(); + if ($type == ArcanistDiffChangeType::TYPE_COPY_HERE) { + $verbs = 'copies'; + } else { + $verbs = 'moves'; + } + $this->confirm( + "Patch {$verbs} '{$path}' to '{$cpath}', but source path ". + "does not exist in the working copy. Continue anyway?"); + } else { + $copies[] = array( + $change->getOldPath(), + $change->getCurrentPath()); + } + break; + case ArcanistDiffChangeType::TYPE_ADD: + $adds[] = $change->getCurrentPath(); + break; + } + if ($should_patch) { + if ($change->getHunks()) { + $cbundle = ArcanistBundle::newFromChanges(array($change)); + $patches[$change->getCurrentPath()] = $cbundle->toUnifiedDiff(); + } + $prop_old = $change->getOldProperties(); + $prop_new = $change->getNewProperties(); + $props = $prop_old + $prop_new; + foreach ($props as $key => $ignored) { + if (idx($prop_old, $key) !== idx($prop_new, $key)) { + $propset[$change->getCurrentPath()][$key] = idx($prop_new[$key]); + } + } + } + } + + foreach ($copies as $copy) { + list($src, $dst) = $copy; + passthru( + csprintf( + '(cd %s; svn cp %s %s)', + $repository_api->getPath(), + $src, + $dst)); + } + + foreach ($deletes as $delete) { + passthru( + csprintf( + '(cd %s; svn rm %s)', + $repository_api->getPath(), + $delete)); + } + + foreach ($patches as $path => $patch) { + $tmp = new TempFile(); + Filesystem::writeFile($tmp, $patch); + passthru( + csprintf( + '(cd %s; patch -p0 < %s)', + $repository_api->getPath(), + $tmp)); + } + + foreach ($adds as $add) { + passthru( + csprintf( + '(cd %s; svn add %s)', + $repository_api->getPath(), + $add)); + } + + foreach ($propset as $path => $changes) { + foreach ($change as $prop => $value) { + // TODO: Probably need to handle svn:executable specially here by + // doing chmod +x or -x. + if ($value === null) { + passthru( + csprintf( + '(cd %s; svn propdel %s %s)', + $repository_api->getPath(), + $prop, + $path)); + } else { + passthru( + csprintf( + '(cd %s; svn propset %s %s %s)', + $repository_api->getPath(), + $prop, + $value, + $path)); + } + } + } + + echo "Applied patch.\n"; + } else { + $future = new ExecFuture( + '(cd %s; git apply --index)', + $repository_api->getPath()); + $future->write($bundle->toGitPatch()); + $future->resolvex(); + } + + return 0; + } +} diff --git a/src/workflow/patch/__init__.php b/src/workflow/patch/__init__.php new file mode 100644 index 00000000..54f54442 --- /dev/null +++ b/src/workflow/patch/__init__.php @@ -0,0 +1,22 @@ + 'paths', + ); + } + + public function requiresWorkingCopy() { + return true; + } + + public function requiresRepositoryAPI() { + return true; + } + + public function run() { + + $working_copy = $this->getWorkingCopy(); + + $engine_class = $this->getArgument( + 'engine', + $working_copy->getConfig('unit_engine')); + + if (!$engine_class) { + throw new ArcanistNoEngineException( + "No unit test engine is configured for this project. Edit .arcconfig ". + "to specify a unit test engine."); + } + + $ok = phutil_autoload_class($engine_class); + if (!$ok) { + throw new ArcanistUsageException( + "Configured unit test engine '{$engine_class}' could not be loaded."); + } + + $repository_api = $this->getRepositoryAPI(); + + if ($this->getArgument('paths')) { + // TODO: deal with git stuff + + $paths = $this->getArgument('paths'); + } else { + $paths = $repository_api->getWorkingCopyStatus(); + $paths = array_keys($paths); + } + + $engine = newv($engine_class, array()); + $engine->setWorkingCopy($working_copy); + $engine->setPaths($paths); + + $results = $engine->run(); + + $status_codes = array( + ArcanistUnitTestResult::RESULT_PASS => phutil_console_format( + ' ** PASS **'), + ArcanistUnitTestResult::RESULT_FAIL => phutil_console_format( + ' ** FAIL **'), + ArcanistUnitTestResult::RESULT_SKIP => phutil_console_format( + ' ** SKIP **'), + ArcanistUnitTestResult::RESULT_BROKEN => phutil_console_format( + ' ** BROKEN **'), + ArcanistUnitTestResult::RESULT_UNSOUND => phutil_console_format( + ' ** UNSOUND **'), + ); + + foreach ($results as $result) { + $result_code = $result->getResult(); + echo $status_codes[$result_code].' '.$result->getName()."\n"; + if ($result_code != ArcanistUnitTestResult::RESULT_PASS) { + echo $result->getUserData()."\n"; + } + } + + $overall_result = self::RESULT_OKAY; + foreach ($results as $result) { + $result_code = $result->getResult(); + if ($result_code == ArcanistUnitTestResult::RESULT_FAIL || + $result_code == ArcanistUnitTestResult::RESULT_BROKEN) { + $overall_result = self::RESULT_FAIL; + break; + } + if ($result_code == ArcanistUnitTestResult::RESULT_UNSOUND) { + $overall_result = self::RESULT_UNSOUND; + break; + } + } + + return $overall_result; + } +} diff --git a/src/workflow/unit/__init__.php b/src/workflow/unit/__init__.php new file mode 100644 index 00000000..9ac1daea --- /dev/null +++ b/src/workflow/unit/__init__.php @@ -0,0 +1,19 @@ +projectRoot = $root; + $this->projectConfig = $config; + } + + public function getProjectID() { + return $this->getConfig('project_id'); + } + + public function getProjectRoot() { + return $this->projectRoot; + } + + public function getConduitURI() { + return $this->getConfig('conduit_uri'); + } + + public function getConfig($key) { + if (!empty($this->projectConfig[$key])) { + return $this->projectConfig[$key]; + } + return null; + } + +} diff --git a/src/workingcopyidentity/__init__.php b/src/workingcopyidentity/__init__.php new file mode 100644 index 00000000..2a30594d --- /dev/null +++ b/src/workingcopyidentity/__init__.php @@ -0,0 +1,21 @@ +/dev/null + +install: xhpast + cp xhpast ../../src/staticanalysis/parsers/xhpast/bin/ + +parser.yacc.cpp: parser.y + bison --debug --verbose -d -o $@ $< + +parser.yacc.hpp: parser.yacc.cpp + +scanner.lex.cpp: scanner.l + `which flex35 2>/dev/null || which flex 2>/dev/null` \ + -C --header-file=scanner.lex.hpp -o $@ -d $< + +scanner.lex.hpp: scanner.lex.cpp + +node_names.hpp: generate_nodes.php + php -f generate_nodes.php + +%.o: %.cpp + $(CXX) -c $(CPPFLAGS) -o $@ $< + +parser.yacc.o: scanner.lex.hpp + +scanner.lex.o: parser.yacc.hpp node_names.hpp + +libxhpast.a: astnode.o scanner.lex.o parser.yacc.o + $(AR) -crs $@ $^ + +xhpast: xhpast.cpp libxhpast.a + $(CXX) $(CPPFLAGS) -o $@ $^ + +.PHONY: all clean tags diff --git a/support/xhpast/ast.hpp b/support/xhpast/ast.hpp new file mode 100755 index 00000000..7548b9ab --- /dev/null +++ b/support/xhpast/ast.hpp @@ -0,0 +1,101 @@ +/* + * Copyright 2011 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "astnode.hpp" + +class yy_extra_type { + public: + yy_extra_type() { + lineno = 1; + terminated = false; + used = false; + short_tags = true; + asp_tags = false; + idx_expr = false; + include_debug = false; + expecting_xhp_class_statements = false; + list_size = 0; + colon_hack = false; + pushStack(); + } + + bool short_tags; // `short_open_tag` in php.ini + bool asp_tags; // `asp_tags` in php.ini + bool idx_expr; // allow code like `foo()['bar']` + bool include_debug; // include line numbers and file names in XHP object creation + size_t first_lineno; // line number before scanning the current token + size_t lineno; // current line number being scanned. + std::string error; // description of error (if terminated true) + bool terminated; // becomes true when the parser terminates with an error + bool used; // were any XHP-specific extensions found in this code? + int last_token; // the last token to be returned by the scanner + int insert_token; // insert this token without reading from buffer + size_t heredoc_yyleng; // last length of yytext while scannling + const char* heredoc_data; // where our heredoc data starts + std::string heredoc_label; // heredoc sentinel label + std::stack curly_stack; // tokens appearing before a { + bool expecting_xhp_class_statements; // when we're one level deep in a class + bool old_expecting_xhp_class_statements; // store old value while inside class method + bool used_attributes; // did this class use the `attribute` keyword + unsigned int list_size; + bool colon_hack; + + xhpast::token_list_t token_list; + + /* Utility functions for checking proper tag closing */ + bool haveTag() { + return !tag_stack.front().empty(); + } + const std::string &peekTag() { + return tag_stack.front().front(); + } + void pushTag(const std::string &tag) { + tag_stack.front().push_front(tag); + } + void popTag() { + tag_stack.front().pop_front(); + } + void pushStack() { + tag_stack.push_front(std::deque()); + } + void popStack() { + tag_stack.pop_front(); + } + + protected: + std::deque > tag_stack; +}; + +#define YYSTYPE xhpast::Node * +#define YY_HEADER_EXPORT_START_CONDITIONS +#define YY_EXTRA_TYPE yy_extra_type* + +#include "parser.yacc.hpp" +#ifndef FLEX_SCANNER + #include "scanner.lex.hpp" +#endif + +int xhpparse(void*, YYSTYPE *); +void xhp_new_push_state(int s, struct yyguts_t* yyg); +void xhp_new_pop_state(struct yyguts_t* yyg); +void xhp_set_state(int s, struct yyguts_t* yyg); diff --git a/support/xhpast/astnode.cpp b/support/xhpast/astnode.cpp new file mode 100755 index 00000000..ac3ece2d --- /dev/null +++ b/support/xhpast/astnode.cpp @@ -0,0 +1,17 @@ +/* + * Copyright 2011 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "astnode.hpp" diff --git a/support/xhpast/astnode.hpp b/support/xhpast/astnode.hpp new file mode 100755 index 00000000..a76dbda1 --- /dev/null +++ b/support/xhpast/astnode.hpp @@ -0,0 +1,126 @@ +/* + * Copyright 2011 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +namespace xhpast { + + class Token; + typedef std::list token_list_t; + + class Token { + + public: + unsigned int type; + std::string value; + unsigned int lineno; + unsigned int n; + + Token(unsigned int type, char *value, unsigned int n) : + type(type), + value(value), + n(n) { + } + }; + + class Node; + typedef std::list node_list_t; + + class Node { + public: + unsigned int type; + + int l_tok; + int r_tok; + + node_list_t children; + + + Node() : type(0), l_tok(-1), r_tok(-1) {}; + + Node(unsigned int type) : type(type), l_tok(-1), r_tok(-1) {}; + + Node(unsigned int type, int end_tok) : + type(type) { + this->l_tok = end_tok; + this->r_tok = end_tok; + } + + Node(unsigned int type, int l_tok, int r_tok) : + type(type), + l_tok(l_tok), + r_tok(r_tok) { + + } + + Node *appendChild(Node *node) { + this->children.push_back(node); + return this->setEnd(node); + } + + Node *appendChildren(Node *node) { + for (node_list_t::iterator ii = node->children.begin(); ii != node->children.end(); ++ii) { + this->children.push_back(*ii); + this->setEnd(*ii); + } + return this; + } + + Node *firstChild() { + return *(this->children.begin()); + } + + Node *setType(unsigned int t) { + this->type = t; + return this; + } + + Node *setEnd(Node *n) { + if (!n) { + fprintf(stderr, "Trying to setEnd() a null node to one of type %d\n", this->type); + exit(1); + } + + if (n->r_tok != -1 && (n->r_tok > this->r_tok || (this->r_tok == -1))) { + this->r_tok = n->r_tok; + } + if (this->l_tok == -1) { + this->l_tok = n->l_tok; + } + return this; + } + + Node *setBegin(Node *n) { + if (!n) { + fprintf(stderr, "Trying to setBegin() a null node to one of type %d\n", this->type); + exit(1); + } + + if (n->l_tok != -1 && (n->l_tok < this->l_tok || (this->l_tok == -1))) { + this->l_tok = n->l_tok; + } + if (this->r_tok == -1) { + this->r_tok = n->r_tok; + } + return this; + } + + }; +} diff --git a/support/xhpast/generate_nodes.php b/support/xhpast/generate_nodes.php new file mode 100755 index 00000000..0a3b57af --- /dev/null +++ b/support/xhpast/generate_nodes.php @@ -0,0 +1,165 @@ +#!/usr/local/bin/php + $value) { + $hpp .= "#define {$node} {$value}\n"; +} +file_put_contents('node_names.hpp', $hpp); +echo "Wrote C++ definition.\n"; + +$php = + " $value) { + $php .= " {$value} => '{$node}',\n"; +} +$php .= " );\n"; +$php .= "}\n"; +file_put_contents('parser_nodes.php', $php); +echo "Wrote PHP definition.\n"; + + + diff --git a/support/xhpast/node_names.hpp b/support/xhpast/node_names.hpp new file mode 100755 index 00000000..8b19f343 --- /dev/null +++ b/support/xhpast/node_names.hpp @@ -0,0 +1,117 @@ +#define n_PROGRAM 9000 +#define n_SYMBOL_NAME 9001 +#define n_HALT_COMPILER 9002 +#define n_NAMESPACE 9003 +#define n_STATEMENT 9004 +#define n_EMPTY 9005 +#define n_STATEMENT_LIST 9006 +#define n_OPEN_TAG 9007 +#define n_CLOSE_TAG 9008 +#define n_USE_LIST 9009 +#define n_USE 9010 +#define n_CONSTANT_DECLARATION_LIST 9011 +#define n_CONSTANT_DECLARATION 9012 +#define n_STRING 9013 +#define n_LABEL 9014 +#define n_CONDITION_LIST 9015 +#define n_CONTROL_CONDITION 9016 +#define n_IF 9017 +#define n_ELSEIF 9018 +#define n_ELSE 9019 +#define n_WHILE 9020 +#define n_DO_WHILE 9021 +#define n_FOR 9022 +#define n_FOR_EXPRESSION 9023 +#define n_SWITCH 9024 +#define n_BREAK 9025 +#define n_CONTINUE 9026 +#define n_RETURN 9027 +#define n_GLOBAL_DECLARATION_LIST 9028 +#define n_GLOBAL_DECLARATION 9029 +#define n_STATIC_DECLARATION_LIST 9030 +#define n_STATIC_DECLARATION 9031 +#define n_ECHO_LIST 9032 +#define n_ECHO 9033 +#define n_INLINE_HTML 9034 +#define n_UNSET_LIST 9035 +#define n_UNSET 9036 +#define n_FOREACH 9037 +#define n_FOREACH_EXPRESSION 9038 +#define n_THROW 9039 +#define n_GOTO 9040 +#define n_TRY 9041 +#define n_CATCH_LIST 9042 +#define n_CATCH 9043 +#define n_DECLARE 9044 +#define n_DECLARE_DECLARATION_LIST 9045 +#define n_DECLARE_DECLARATION 9046 +#define n_VARIABLE 9047 +#define n_REFERENCE 9048 +#define n_VARIABLE_REFERENCE 9049 +#define n_FUNCTION_DECLARATION 9050 +#define n_CLASS_DECLARATION 9051 +#define n_CLASS_ATTRIBUTES 9052 +#define n_EXTENDS 9053 +#define n_EXTENDS_LIST 9054 +#define n_IMPLEMENTS_LIST 9055 +#define n_INTERFACE_DECLARATION 9056 +#define n_CASE 9057 +#define n_DEFAULT 9058 +#define n_DECLARATION_PARAMETER_LIST 9059 +#define n_DECLARATION_PARAMETER 9060 +#define n_TYPE_NAME 9061 +#define n_VARIABLE_VARIABLE 9062 +#define n_CLASS_MEMBER_DECLARATION_LIST 9063 +#define n_CLASS_MEMBER_DECLARATION 9064 +#define n_CLASS_CONSTANT_DECLARATION_LIST 9065 +#define n_CLASS_CONSTANT_DECLARATION 9066 +#define n_METHOD_DECLARATION 9067 +#define n_METHOD_MODIFIER_LIST 9068 +#define n_FUNCTION_MODIFIER_LIST 9069 +#define n_CLASS_MEMBER_MODIFIER_LIST 9070 +#define n_EXPRESSION_LIST 9071 +#define n_LIST 9072 +#define n_ASSIGNMENT 9073 +#define n_NEW 9074 +#define n_UNARY_PREFIX_EXPRESSION 9075 +#define n_UNARY_POSTFIX_EXPRESSION 9076 +#define n_BINARY_EXPRESSION 9077 +#define n_TERNARY_EXPRESSION 9078 +#define n_CAST_EXPRESSION 9079 +#define n_CAST 9080 +#define n_OPERATOR 9081 +#define n_ARRAY_LITERAL 9082 +#define n_EXIT_EXPRESSION 9083 +#define n_BACKTICKS_EXPRESSION 9084 +#define n_LEXICAL_VARIABLE_LIST 9085 +#define n_NUMERIC_SCALAR 9086 +#define n_STRING_SCALAR 9087 +#define n_MAGIC_SCALAR 9088 +#define n_CLASS_STATIC_ACCESS 9089 +#define n_CLASS_NAME 9090 +#define n_MAGIC_CLASS_KEYWORD 9091 +#define n_OBJECT_PROPERTY_ACCESS 9092 +#define n_ARRAY_VALUE_LIST 9093 +#define n_ARRAY_VALUE 9094 +#define n_CALL_PARAMETER_LIST 9095 +#define n_VARIABLE_EXPRESSION 9096 +#define n_INCLUDE_FILE 9097 +#define n_HEREDOC 9098 +#define n_FUNCTION_CALL 9099 +#define n_INDEX_ACCESS 9100 +#define n_ASSIGNMENT_LIST 9101 +#define n_METHOD_CALL 9102 +#define n_XHP_TAG 9103 +#define n_XHP_TAG_OPEN 9104 +#define n_XHP_TAG_CLOSE 9105 +#define n_XHP_TEXT 9106 +#define n_XHP_EXPRESSION 9107 +#define n_XHP_ATTRIBUTE_LIST 9108 +#define n_XHP_ATTRIBUTE 9109 +#define n_XHP_LITERAL 9110 +#define n_XHP_ATTRIBUTE_LITERAL 9111 +#define n_XHP_ATTRIBUTE_EXPRESSION 9112 +#define n_XHP_NODE_LIST 9113 +#define n_XHP_ENTITY 9114 +#define n_CONCATENATION_LIST 9115 +#define n_PARENTHETICAL_EXPRESSION 9116 diff --git a/support/xhpast/parser.y b/support/xhpast/parser.y new file mode 100755 index 00000000..2a5e2679 --- /dev/null +++ b/support/xhpast/parser.y @@ -0,0 +1,2915 @@ +/* + * Copyright 2011 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +%{ +#include "ast.hpp" +#include "node_names.hpp" +// PHP's if/else rules use right reduction rather than left reduction which +// means while parsing nested if/else's the stack grows until it the last +// statement is read. This is annoying, particularly because of a quirk in +// bison. +// http://www.gnu.org/software/bison/manual/html_node/Memory-Management.html +// Apparently if you compile a bison parser with g++ it can no longer grow +// the stack. The work around is to just make your initial stack ridiculously +// large. Unfortunately that increases memory usage while parsing which is +// dumb. Anyway, putting a TODO here to fix PHP's if/else grammar. +#define YYINITDEPTH 500 +%} + +%{ +#undef yyextra +#define yyextra static_cast(xhpastget_extra(yyscanner)) +#undef yylineno +#define yylineno yyextra->first_lineno +#define push_state(s) xhp_new_push_state(s, (struct yyguts_t*) yyscanner) +#define pop_state() xhp_new_pop_state((struct yyguts_t*) yyscanner) +#define set_state(s) xhp_set_state(s, (struct yyguts_t*) yyscanner) + +#define NNEW(t) \ + (new xhpast::Node(t)) + +#define NTYPE(n, type) \ + ((n)->setType(type)) + +#define NMORE(n, end) \ + ((n)->setEnd(end)) + +#define NSPAN(n, type, end) \ + (NMORE(NTYPE((n), type), end)) + +#define NLMORE(n, begin) \ + ((n)->setBegin(begin)) + +#define NEXPAND(l, n, r) \ + ((n)->setBegin(l)->setEnd(r)) + +using namespace std; + +static void yyerror(void* yyscanner, void* _, const char* error) { + if (yyextra->terminated) { + return; + } + yyextra->terminated = true; + yyextra->error = error; +} + +static void replacestr(string &source, const string &find, const string &rep) { + size_t j; + while ((j = source.find(find)) != std::string::npos) { + source.replace(j, find.length(), rep); + } +} + +%} + +%expect 9 +// 2: PHP's if/else grammar +// 7: expr '[' dim_offset ']' -- shift will default to first grammar +%name-prefix = "xhpast" +%pure-parser +%parse-param { void* yyscanner } +%parse-param { xhpast::Node** root } +%lex-param { void* yyscanner } +%error-verbose + +%left T_INCLUDE T_INCLUDE_ONCE T_EVAL T_REQUIRE T_REQUIRE_ONCE +%left ',' +%left T_LOGICAL_OR +%left T_LOGICAL_XOR +%left T_LOGICAL_AND +%right T_PRINT +%left '=' T_PLUS_EQUAL T_MINUS_EQUAL T_MUL_EQUAL T_DIV_EQUAL T_CONCAT_EQUAL T_MOD_EQUAL T_AND_EQUAL T_OR_EQUAL T_XOR_EQUAL T_SL_EQUAL T_SR_EQUAL +%left '?' ':' +%left T_BOOLEAN_OR +%left T_BOOLEAN_AND +%left '|' +%left '^' +%left '&' +%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL +%nonassoc '<' T_IS_SMALLER_OR_EQUAL '>' T_IS_GREATER_OR_EQUAL +%left T_SL T_SR +%left '+' '-' '.' +%left '*' '/' '%' +%right '!' +%nonassoc T_INSTANCEOF +%right '~' T_INC T_DEC T_INT_CAST T_DOUBLE_CAST T_STRING_CAST T_UNICODE_CAST T_BINARY_CAST T_ARRAY_CAST T_OBJECT_CAST T_BOOL_CAST T_UNSET_CAST '@' +%right '[' +%nonassoc T_NEW T_CLONE +%token T_EXIT +%token T_IF +%left T_ELSEIF +%left T_ELSE +%left T_ENDIF + +%token T_LNUMBER +%token T_DNUMBER +%token T_STRING +%token T_STRING_VARNAME /* unused in XHP: `foo` in `"$foo"` */ +%token T_VARIABLE +%token T_NUM_STRING /* unused in XHP: `0` in `"$foo[0]"` */ +%token T_INLINE_HTML +%token T_CHARACTER /* unused in vanilla PHP */ +%token T_BAD_CHARACTER /* unused in vanilla PHP */ +%token T_ENCAPSED_AND_WHITESPACE /* unused in XHP: ` ` in `" "` */ +%token T_CONSTANT_ENCAPSED_STRING /* overloaded in XHP; replaces '"' encaps_list '"' */ +%token T_BACKTICKS_EXPR /* new in XHP; replaces '`' backticks_expr '`' */ +%token T_ECHO +%token T_DO +%token T_WHILE +%token T_ENDWHILE +%token T_FOR +%token T_ENDFOR +%token T_FOREACH +%token T_ENDFOREACH +%token T_DECLARE +%token T_ENDDECLARE +%token T_AS +%token T_SWITCH +%token T_ENDSWITCH +%token T_CASE +%token T_DEFAULT +%token T_BREAK +%token T_CONTINUE +%token T_GOTO +%token T_FUNCTION +%token T_CONST +%token T_RETURN +%token T_TRY +%token T_CATCH +%token T_THROW +%token T_USE +%token T_GLOBAL +%right T_STATIC T_ABSTRACT T_FINAL T_PRIVATE T_PROTECTED T_PUBLIC +%token T_VAR +%token T_UNSET +%token T_ISSET +%token T_EMPTY +%token T_HALT_COMPILER +%token T_CLASS +%token T_INTERFACE +%token T_EXTENDS +%token T_IMPLEMENTS +%token T_OBJECT_OPERATOR +%token T_DOUBLE_ARROW +%token T_LIST +%token T_ARRAY +%token T_CLASS_C +%token T_METHOD_C +%token T_FUNC_C +%token T_LINE +%token T_FILE +%token T_COMMENT +%token T_DOC_COMMENT +%token T_OPEN_TAG +%token T_OPEN_TAG_WITH_ECHO +%token T_OPEN_TAG_FAKE +%token T_CLOSE_TAG +%token T_WHITESPACE +%token T_START_HEREDOC /* unused in XHP; replaced with T_HEREDOC */ +%token T_END_HEREDOC /* unused in XHP; replaced with T_HEREDOC */ +%token T_HEREDOC /* new in XHP; replaces start_heredoc encaps_list T_END_HEREDOC */ +%token T_DOLLAR_OPEN_CURLY_BRACES /* unused in XHP: `${` in `"${foo}"` */ +%token T_CURLY_OPEN /* unused in XHP: `{$` in `"{$foo}"` */ +%token T_PAAMAYIM_NEKUDOTAYIM +%token T_BINARY_DOUBLE /* unsused in XHP: `b"` in `b"foo"` */ +%token T_BINARY_HEREDOC /* unsused in XHP: `b<<<` in `b<<appendChild($1); + } +; + +top_statement_list: + top_statement_list top_statement { + $$ = $1->appendChild($2); + } +| /* empty */ { + $$ = NNEW(n_STATEMENT_LIST); + } +; + +namespace_name: + T_STRING { + $$ = NTYPE($1, n_SYMBOL_NAME); + } +| namespace_name T_NS_SEPARATOR T_STRING { + $$ = NMORE($1, $3); + } +; + +top_statement: + statement +| function_declaration_statement +| class_declaration_statement +| T_HALT_COMPILER '(' ')' ';' { + $1 = NSPAN($1, n_HALT_COMPILER, $3); + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $4); + } +| T_NAMESPACE namespace_name ';' { + NSPAN($1, n_NAMESPACE, $2); + $1->appendChild($2); + $1->appendChild(NNEW(n_EMPTY)); + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $3); + } +| T_NAMESPACE namespace_name '{' top_statement_list '}' { + NSPAN($1, n_NAMESPACE, $5); + $1->appendChild($2); + NMORE($4, $5); + NLMORE($4, $3); + $1->appendChild($4); + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +| T_NAMESPACE '{' top_statement_list '}' { + NSPAN($1, n_NAMESPACE, $4); + $1->appendChild(NNEW(n_EMPTY)); + NMORE($3, $4); + NLMORE($3, $2); + $1->appendChild($3); + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +| T_USE use_declarations ';' { + NSPAN($1, n_USE, $2); + $1->appendChild($2); + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $3); + } +| constant_declaration ';' { + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $2); + } +; + +use_declarations: + use_declarations ',' use_declaration { + $$ = $1->appendChild($3); + } +| use_declaration { + $$ = NNEW(n_USE_LIST); + $$->appendChild($1); + } +; + +use_declaration: + namespace_name { + $$ = NNEW(n_USE); + $$->appendChild($1); + $$->appendChild(NNEW(n_EMPTY)); + } +| namespace_name T_AS T_STRING { + $$ = NNEW(n_USE); + $$->appendChild($1); + NTYPE($3, n_STRING); + $$->appendChild($3); + } +| T_NS_SEPARATOR namespace_name { + $$ = NNEW(n_USE); + NLMORE($2, $1); + $$->appendChild($2); + $$->appendChild(NNEW(n_EMPTY)); + } +| T_NS_SEPARATOR namespace_name T_AS T_STRING { + $$ = NNEW(n_USE); + NLMORE($2, $1); + $$->appendChild($2); + NTYPE($4, n_STRING); + $$->appendChild($4); + } +; + +constant_declaration: + constant_declaration ',' T_STRING '=' static_scalar { + NMORE($$, $5); + $$->appendChild( + NNEW(n_CONSTANT_DECLARATION) + ->appendChild(NTYPE($3, n_STRING)) + ->appendChild($5)); + } +| T_CONST T_STRING '=' static_scalar { + NSPAN($$, n_CONSTANT_DECLARATION_LIST, $4); + $$->appendChild( + NNEW(n_CONSTANT_DECLARATION) + ->appendChild(NTYPE($2, n_STRING)) + ->appendChild($4)); + } +; + +inner_statement_list: + inner_statement_list inner_statement { + $$ = $1->appendChild($2); + } +| /* empty */ { + $$ = NNEW(n_STATEMENT_LIST); + } +; + +inner_statement: + statement +| function_declaration_statement +| class_declaration_statement +| T_HALT_COMPILER '(' ')' ';' { + $1 = NSPAN($1, n_HALT_COMPILER, $3); + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $4); + } +; + +statement: + unticked_statement +| T_STRING ':' { + NTYPE($1, n_STRING); + $$ = NNEW(n_LABEL); + $$->appendChild($1); + NMORE($$, $2); + } +| T_OPEN_TAG { + $$ = NTYPE($1, n_OPEN_TAG); + } +| T_OPEN_TAG_WITH_ECHO { + $$ = NTYPE($1, n_OPEN_TAG); + } +| T_CLOSE_TAG { + $$ = NTYPE($1, n_CLOSE_TAG); + } +; + +unticked_statement: + '{' inner_statement_list '}' { + NMORE($2, $3); + NLMORE($2, $1); + $$ = $2; + } +| T_IF '(' expr ')' statement elseif_list else_single { + $$ = NNEW(n_CONDITION_LIST); + + $1 = NTYPE($1, n_IF); + $1->appendChild(NSPAN($2, n_CONTROL_CONDITION, $4)->appendChild($3)); + $1->appendChild($5); + + $$->appendChild($1); + $$->appendChildren($6); + + // Hacks: merge a list of if (x) { } else if (y) { } into a single condition + // list instead of a condition tree. + + if ($7->type == n_EMPTY) { + // Ignore. + } else if ($7->type == n_ELSE) { + xhpast::Node *stype = $7->firstChild()->firstChild(); + if (stype && stype->type == n_CONDITION_LIST) { + NTYPE(stype->firstChild(), n_ELSEIF); + stype->firstChild()->l_tok = $7->l_tok; + $$->appendChildren(stype); + } else { + $$->appendChild($7); + } + } else { + $$->appendChild($7); + } + + $$ = NNEW(n_STATEMENT)->appendChild($$); + } +| T_IF '(' expr ')' ':' inner_statement_list new_elseif_list new_else_single T_ENDIF ';' { + + $$ = NNEW(n_CONDITION_LIST); + NTYPE($1, n_IF); + $1->appendChild(NSPAN($2, n_CONTROL_CONDITION, $4)->appendChild($3)); + $1->appendChild($6); + + $$->appendChild($1); + $$->appendChildren($7); + $$->appendChild($8); + NMORE($$, $9); + + $$ = NNEW(n_STATEMENT)->appendChild($$); + NMORE($$, $10); + } +| T_WHILE '(' expr ')' while_statement { + NTYPE($1, n_WHILE); + $1->appendChild(NSPAN($2, n_CONTROL_CONDITION, $4)->appendChild($3)); + $1->appendChild($5); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +| T_DO statement T_WHILE '(' expr ')' ';' { + NTYPE($1, n_DO_WHILE); + $1->appendChild($2); + $1->appendChild(NSPAN($4, n_CONTROL_CONDITION, $6)->appendChild($5)); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $7); + } +| T_FOR '(' for_expr ';' for_expr ';' for_expr ')' for_statement { + NTYPE($1, n_FOR); + + NSPAN($2, n_FOR_EXPRESSION, $8) + ->appendChild($3) + ->appendChild($5) + ->appendChild($7); + + $1->appendChild($2); + $1->appendChild($9); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +| T_SWITCH '(' expr ')' switch_case_list { + NTYPE($1, n_SWITCH); + $1->appendChild(NSPAN($2, n_CONTROL_CONDITION, $4)->appendChild($3)); + $1->appendChild($5); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +| T_BREAK ';' { + NTYPE($1, n_BREAK); + $1->appendChild(NNEW(n_EMPTY)); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $2); + } +| T_BREAK expr ';' { + NTYPE($1, n_BREAK); + $1->appendChild($2); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $3); + } +| T_CONTINUE ';' { + NTYPE($1, n_CONTINUE); + $1->appendChild(NNEW(n_EMPTY)); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $2); + } +| T_CONTINUE expr ';' { + NTYPE($1, n_CONTINUE); + $1->appendChild($2); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $3); + } +| T_RETURN ';' { + NTYPE($1, n_RETURN); + $1->appendChild(NNEW(n_EMPTY)); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $2); + } +| T_RETURN expr_without_variable ';' { + NTYPE($1, n_RETURN); + $1->appendChild($2); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $3); + } +| T_RETURN variable ';' { + NTYPE($1, n_RETURN); + $1->appendChild($2); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $3); + } +| T_GLOBAL global_var_list ';' { + NLMORE($2, $1); + $$ = NNEW(n_STATEMENT)->appendChild($2); + NMORE($$, $3); + } +| T_STATIC static_var_list ';' { + NLMORE($2, $1); + $$ = NNEW(n_STATEMENT)->appendChild($2); + NMORE($$, $3); + } +| T_ECHO echo_expr_list ';' { + NLMORE($2, $1); + $$ = NNEW(n_STATEMENT)->appendChild($2); + NMORE($$, $3); + } +| T_INLINE_HTML { + NTYPE($1, n_INLINE_HTML); + $$ = $1; + } +| expr ';' { + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $2); + } +| T_UNSET '(' unset_variables ')' ';' { + NMORE($3, $4); + NLMORE($3, $1); + $$ = NNEW(n_STATEMENT)->appendChild($3); + NMORE($$, $5); + } +| T_FOREACH '(' variable T_AS foreach_variable foreach_optional_arg ')' foreach_statement { + NTYPE($1, n_FOREACH); + NSPAN($2, n_FOREACH_EXPRESSION, $7); + $2->appendChild($3); + if ($6->type == n_EMPTY) { + $2->appendChild($6); + $2->appendChild($5); + } else { + $2->appendChild($5); + $2->appendChild($6); + } + $1->appendChild($2); + + $1->appendChild($8); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +| T_FOREACH '(' expr_without_variable T_AS variable foreach_optional_arg ')' foreach_statement { + NTYPE($1, n_FOREACH); + NSPAN($2, n_FOREACH_EXPRESSION, $7); + $2->appendChild($3); + if ($6->type == n_EMPTY) { + $2->appendChild($6); + $2->appendChild($5); + } else { + $2->appendChild($5); + $2->appendChild($6); + } + $1->appendChild($2); + $1->appendChild($8); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +| T_DECLARE '(' declare_list ')' declare_statement { + NTYPE($1, n_DECLARE); + $1->appendChild($3); + $1->appendChild($5); + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +| ';' /* empty statement */ { + $$ = NNEW(n_STATEMENT)->appendChild(NNEW(n_EMPTY)); + NMORE($$, $1); + } +| T_TRY '{' inner_statement_list '}' T_CATCH '(' fully_qualified_class_name T_VARIABLE ')' '{' inner_statement_list '}' additional_catches { + NTYPE($1, n_TRY); + $1->appendChild($3); + + NTYPE($5, n_CATCH); + $5->appendChild($7); + $5->appendChild(NTYPE($8, n_VARIABLE)); + $5->appendChild($11); + + $1->appendChild(NNEW(n_CATCH_LIST)->appendChild($5)->appendChildren($13)); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +| T_THROW expr ';' { + NTYPE($1, n_THROW); + $1->appendChild($2); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $3); + + } +| T_GOTO T_STRING ';' { + NTYPE($1, n_GOTO); + NTYPE($2, n_STRING); + $1->appendChild($2); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $3); + } +; + +additional_catches: + non_empty_additional_catches +| /* empty */ { + $$ = NNEW(n_EMPTY); + } +; + +non_empty_additional_catches: + additional_catch { + $$ = NNEW(n_CATCH_LIST); + $$->appendChild($1); + } +| non_empty_additional_catches additional_catch { + $1->appendChild($2); + $$ = $1; + } +; + +additional_catch: + T_CATCH '(' fully_qualified_class_name T_VARIABLE ')' '{' inner_statement_list '}' { + NTYPE($1, n_CATCH); + $1->appendChild($3); + $1->appendChild(NTYPE($4, n_VARIABLE)); + $1->appendChild($7); + NMORE($1, $8); + $$ = $1; + } +; + +unset_variables: + unset_variable { + $$ = NNEW(n_UNSET_LIST); + $$->appendChild($1); + } +| unset_variables ',' unset_variable { + $1->appendChild($3); + $$ = $1; + } +; + +unset_variable: + variable +; + +function_declaration_statement: + unticked_function_declaration_statement +; + +class_declaration_statement: + unticked_class_declaration_statement +; + +is_reference: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| '&' { + $$ = NTYPE($1, n_REFERENCE); + } +; + +unticked_function_declaration_statement: + function is_reference T_STRING '(' parameter_list ')' '{' inner_statement_list '}' { + NSPAN($1, n_FUNCTION_DECLARATION, $9); + $1->appendChild(NNEW(n_EMPTY)); + $1->appendChild($2); + $1->appendChild(NTYPE($3, n_STRING)); + $1->appendChild(NEXPAND($4, $5, $6)); + $$->appendChild(NNEW(n_EMPTY)); + $1->appendChild($8); + + $$ = NNEW(n_STATEMENT)->appendChild($1); + } +; + +unticked_class_declaration_statement: + class_entry_type T_STRING extends_from implements_list '{' class_statement_list '}' { + $$ = NNEW(n_CLASS_DECLARATION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_CLASS_NAME)); + $$->appendChild($3); + $$->appendChild($4); + $$->appendChild($6); + NMORE($$, $7); + + $$ = NNEW(n_STATEMENT)->appendChild($$); + } +| interface_entry T_STRING interface_extends_list '{' class_statement_list '}' { + $$ = NNEW(n_INTERFACE_DECLARATION); + $$->appendChild(NNEW(n_EMPTY)); + NLMORE($$, $1); + $$->appendChild(NTYPE($2, n_CLASS_NAME)); + $$->appendChild($3); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild($5); + NMORE($$, $6); + + $$ = NNEW(n_STATEMENT)->appendChild($$); + } +; + +class_entry_type: + T_CLASS { + NTYPE($1, n_CLASS_ATTRIBUTES); + $1->appendChild(NNEW(n_EMPTY)); + $$ = $1; + } +| T_ABSTRACT T_CLASS { + NTYPE($2, n_CLASS_ATTRIBUTES); + NLMORE($2, $1); + $2->appendChild(NTYPE($1, n_STRING)); + + $$ = $1; + } +| T_FINAL T_CLASS { + NTYPE($2, n_CLASS_ATTRIBUTES); + NLMORE($2, $1); + $2->appendChild(NTYPE($1, n_STRING)); + + $$ = $1; + } +; + +extends_from: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| T_EXTENDS fully_qualified_class_name { + $$ = NTYPE($1, n_EXTENDS_LIST)->appendChild($2); + } +; + +interface_entry: + T_INTERFACE +; + +interface_extends_list: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| T_EXTENDS interface_list { + NTYPE($1, n_EXTENDS_LIST); + $1->appendChildren($2); + $$ = $1; + } +; + +implements_list: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| T_IMPLEMENTS interface_list { + NTYPE($1, n_IMPLEMENTS_LIST); + $1->appendChildren($2); + $$ = $1; + } +; + +interface_list: + fully_qualified_class_name { + $$ = NNEW(n_IMPLEMENTS_LIST)->appendChild($1); + } +| interface_list ',' fully_qualified_class_name { + $$ = $1->appendChild($3); + } +; + +foreach_optional_arg: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| T_DOUBLE_ARROW foreach_variable { + $$ = $2; + } +; + +foreach_variable: + variable +| '&' variable { + NTYPE($1, n_VARIABLE_REFERENCE); + $1->appendChild($2); + $$ = $1; + } +; + +for_statement: + statement +| ':' inner_statement_list T_ENDFOR ';' { + NLMORE($2, $1); + NMORE($2, $4); + $$ = $2; + } +; + +foreach_statement: + statement +| ':' inner_statement_list T_ENDFOREACH ';' { + NLMORE($2, $1); + NMORE($2, $4); + $$ = $2; + } +; + +declare_statement: + statement +| ':' inner_statement_list T_ENDDECLARE ';' { + NLMORE($2, $1); + NMORE($2, $4); + $$ = $2; + } +; + +declare_list: + T_STRING '=' static_scalar { + $$ = NNEW(n_DECLARE_DECLARATION); + $$->appendChild(NTYPE($1, n_STRING)); + $$->appendChild($3); + $$ = NNEW(n_DECLARE_DECLARATION_LIST)->appendChild($$); + } +| declare_list ',' T_STRING '=' static_scalar { + $$ = NNEW(n_DECLARE_DECLARATION); + $$->appendChild(NTYPE($3, n_STRING)); + $$->appendChild($5); + + $1->appendChild($$); + $$ = $1; + } +; + +switch_case_list: + '{' case_list '}' { + NMORE($2, $3); + NLMORE($2, $1); + $$ = $2; + } +| '{' ';' case_list '}' { + // ...why does this rule exist? + + NTYPE($2, n_STATEMENT); + $1->appendChild(NNEW(n_EMPTY)); + + $$ = NNEW(n_STATEMENT_LIST)->appendChild($2); + $$->appendChildren($3); + NMORE($$, $4); + NLMORE($$, $1); + } +| ':' case_list T_ENDSWITCH ';' { + NMORE($2, $4); + NLMORE($2, $1); + $$ = $2; + } +| ':' ';' case_list T_ENDSWITCH ';' { + NTYPE($2, n_STATEMENT); + $1->appendChild(NNEW(n_EMPTY)); + + $$ = NNEW(n_STATEMENT_LIST)->appendChild($2); + $$->appendChildren($3); + NMORE($$, $5); + NLMORE($$, $1); + } +; + +case_list: + /* empty */ { + $$ = NNEW(n_STATEMENT_LIST); + } +| case_list T_CASE expr case_separator inner_statement_list { + NTYPE($2, n_CASE); + $2->appendChild($3); + $2->appendChild($5); + + $1->appendChild($2); + $$ = $1; + } +| case_list T_DEFAULT case_separator inner_statement_list { + NTYPE($2, n_DEFAULT); + $2->appendChild($4); + + $1->appendChild($2); + $$ = $1; + } +; + +case_separator: + ':' +| ';' +; + +while_statement: + statement +| ':' inner_statement_list T_ENDWHILE ';' { + NMORE($2, $4); + NLMORE($2, $1); + $$ = $2; + } +; + +elseif_list: + /* empty */ { + $$ = NNEW(n_CONDITION_LIST); + } +| elseif_list T_ELSEIF '(' expr ')' statement { + NTYPE($2, n_ELSEIF); + $2->appendChild(NSPAN($3, n_CONTROL_CONDITION, $5)->appendChild($4)); + $2->appendChild($6); + + $$ = $1->appendChild($2); + } +; + +new_elseif_list: + /* empty */ { + $$ = NNEW(n_CONDITION_LIST); + } +| new_elseif_list T_ELSEIF '(' expr ')' ':' inner_statement_list { + NTYPE($2, n_ELSEIF); + $2->appendChild($4); + $2->appendChild($7); + + $$ = $1->appendChild($2); + } +; + +else_single: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| T_ELSE statement { + NTYPE($1, n_ELSE); + $1->appendChild($2); + $$ = $1; + } +; + +new_else_single: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| T_ELSE ':' inner_statement_list { + NTYPE($1, n_ELSE); + $1->appendChild($3); + $$ = $1; + } +; + +parameter_list: + non_empty_parameter_list +| /* empty */ { + $$ = NNEW(n_DECLARATION_PARAMETER_LIST); + } +; + +non_empty_parameter_list: + optional_class_type T_VARIABLE { + $$ = NNEW(n_DECLARATION_PARAMETER); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_VARIABLE)); + $$->appendChild(NNEW(n_EMPTY)); + + $$ = NNEW(n_DECLARATION_PARAMETER_LIST)->appendChild($$); + } +| optional_class_type '&' T_VARIABLE { + $$ = NNEW(n_DECLARATION_PARAMETER); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_VARIABLE_REFERENCE)); + $2->appendChild(NTYPE($3, n_VARIABLE)); + $$->appendChild(NNEW(n_EMPTY)); + + $$ = NNEW(n_DECLARATION_PARAMETER_LIST)->appendChild($$); + } +| optional_class_type '&' T_VARIABLE '=' static_scalar { + $$ = NNEW(n_DECLARATION_PARAMETER); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_VARIABLE_REFERENCE)); + $2->appendChild(NTYPE($3, n_VARIABLE)); + $$->appendChild($5); + + $$ = NNEW(n_DECLARATION_PARAMETER_LIST)->appendChild($$); + } +| optional_class_type T_VARIABLE '=' static_scalar { + $$ = NNEW(n_DECLARATION_PARAMETER); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_VARIABLE)); + $$->appendChild($4); + + $$ = NNEW(n_DECLARATION_PARAMETER_LIST)->appendChild($$); + } +| non_empty_parameter_list ',' optional_class_type T_VARIABLE { + $$ = NNEW(n_DECLARATION_PARAMETER); + $$->appendChild($3); + $$->appendChild(NTYPE($4, n_VARIABLE)); + $$->appendChild(NNEW(n_EMPTY)); + + $$ = $1->appendChild($$); + } +| non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE { + $$ = NNEW(n_DECLARATION_PARAMETER); + $$->appendChild($3); + $$->appendChild(NTYPE($4, n_VARIABLE_REFERENCE)); + $4->appendChild(NTYPE($5, n_VARIABLE)); + $$->appendChild(NNEW(n_EMPTY)); + + $$ = $1->appendChild($$); + } +| non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE '=' static_scalar { + $$ = NNEW(n_DECLARATION_PARAMETER); + $$->appendChild($3); + $$->appendChild(NTYPE($4, n_VARIABLE_REFERENCE)); + $4->appendChild(NTYPE($5, n_VARIABLE)); + $$->appendChild($7); + + $$ = $1->appendChild($$); + } +| non_empty_parameter_list ',' optional_class_type T_VARIABLE '=' static_scalar { + $$ = NNEW(n_DECLARATION_PARAMETER); + $$->appendChild($3); + $$->appendChild(NTYPE($4, n_VARIABLE)); + $$->appendChild($6); + + $$ = $1->appendChild($$); + } +; + +optional_class_type: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| fully_qualified_class_name { + $$ = $1; + } +| T_ARRAY { + $$ = NTYPE($1, n_TYPE_NAME); + } +; + +function_call_parameter_list: + non_empty_function_call_parameter_list +| /* empty */ { + $$ = NNEW(n_CALL_PARAMETER_LIST); + } +; + +non_empty_function_call_parameter_list: + expr_without_variable { + $$ = NNEW(n_CALL_PARAMETER_LIST)->appendChild($1); + } +| variable { + $$ = NNEW(n_CALL_PARAMETER_LIST)->appendChild($1); + } +| '&' w_variable { + NTYPE($1, n_VARIABLE_REFERENCE); + $1->appendChild($2); + $$ = NNEW(n_CALL_PARAMETER_LIST)->appendChild($1); + } +| non_empty_function_call_parameter_list ',' expr_without_variable { + $$ = $1->appendChild($3); + } +| non_empty_function_call_parameter_list ',' variable { + $$ = $1->appendChild($3); + } +| non_empty_function_call_parameter_list ',' '&' w_variable { + $$ = $1->appendChild($3); + } +; + +global_var_list: + global_var_list ',' global_var { + $1->appendChild($3); + $$ = $1; + } +| global_var { + $$ = NNEW(n_GLOBAL_DECLARATION_LIST); + $$->appendChild($1); + } +; + +global_var: + T_VARIABLE { + $$ = NTYPE($1, n_VARIABLE); + } +| '$' r_variable { + $$ = NTYPE($1, n_VARIABLE_VARIABLE); + } +| '$' '{' expr '}' { + $$ = NTYPE($1, n_VARIABLE_VARIABLE); + } +; + +static_var_list: + static_var_list ',' T_VARIABLE { + NTYPE($3, n_VARIABLE); + $$ = NNEW(n_STATIC_DECLARATION); + $$->appendChild($3); + $$->appendChild(NNEW(n_EMPTY)); + + $$ = $1->appendChild($$); + } +| static_var_list ',' T_VARIABLE '=' static_scalar { + NTYPE($3, n_VARIABLE); + $$ = NNEW(n_STATIC_DECLARATION); + $$->appendChild($3); + $$->appendChild($5); + + $$ = $1->appendChild($$); + } +| T_VARIABLE { + NTYPE($1, n_VARIABLE); + $$ = NNEW(n_STATIC_DECLARATION); + $$->appendChild($1); + $$->appendChild(NNEW(n_EMPTY)); + + $$ = NNEW(n_STATIC_DECLARATION_LIST)->appendChild($$); + } +| T_VARIABLE '=' static_scalar { + NTYPE($1, n_VARIABLE); + $$ = NNEW(n_STATIC_DECLARATION); + $$->appendChild($1); + $$->appendChild($3); + + $$ = NNEW(n_STATIC_DECLARATION_LIST)->appendChild($$); + } +; + +class_statement_list: + class_statement_list class_statement { + $$ = $1->appendChild($2); + } +| /* empty */ { + $$ = NNEW(n_STATEMENT_LIST); + } +; + +class_statement: + variable_modifiers class_variable_declaration ';' { + $$ = NNEW(n_CLASS_MEMBER_DECLARATION_LIST); + $$->appendChild($1); + $$->appendChildren($2); + + $$ = NNEW(n_STATEMENT)->appendChild($$); + NMORE($$, $3); + } +| class_constant_declaration ';' { + $$ = NNEW(n_STATEMENT)->appendChild($1); + NMORE($$, $2); + } +| method_modifiers function { + yyextra->old_expecting_xhp_class_statements = yyextra->expecting_xhp_class_statements; + yyextra->expecting_xhp_class_statements = false; + } is_reference T_STRING '(' parameter_list ')' method_body { + yyextra->expecting_xhp_class_statements = yyextra->old_expecting_xhp_class_statements; + + $$ = NNEW(n_METHOD_DECLARATION); + $$->appendChild($1); + $$->appendChild($4); + $$->appendChild(NTYPE($5, n_STRING)); + $$->appendChild(NEXPAND($6, $7, $8)); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild($9); + + $$ = NNEW(n_STATEMENT)->appendChild($$); + } +; + +method_body: + ';' /* abstract method */ { + $$ = NNEW(n_EMPTY); + } +| '{' inner_statement_list '}' { + NMORE($2, $3); + NLMORE($2, $1); + $$ = $2; + } +; + +variable_modifiers: + non_empty_member_modifiers +| T_VAR { + $$ = NNEW(n_CLASS_MEMBER_MODIFIER_LIST); + $$->appendChild(NTYPE($1, n_STRING)); + } +; + +method_modifiers: + /* empty */ { + $$ = NNEW(n_METHOD_MODIFIER_LIST); + } +| non_empty_member_modifiers { + NTYPE($1, n_METHOD_MODIFIER_LIST); + $$ = $1; + } +; + +non_empty_member_modifiers: + member_modifier { + $$ = NNEW(n_CLASS_MEMBER_MODIFIER_LIST); + $$->appendChild(NTYPE($1, n_STRING)); + } +| non_empty_member_modifiers member_modifier { + $$ = $1->appendChild(NTYPE($2, n_STRING)); + } +; + +member_modifier: + T_PUBLIC +| T_PROTECTED +| T_PRIVATE +| T_STATIC +| T_ABSTRACT +| T_FINAL +; + +class_variable_declaration: + class_variable_declaration ',' T_VARIABLE { + $$ = NNEW(n_CLASS_MEMBER_DECLARATION); + $$->appendChild(NTYPE($3, n_VARIABLE)); + $$->appendChild(NNEW(n_EMPTY)); + + $$ = $1->appendChild($$); + } +| class_variable_declaration ',' T_VARIABLE '=' static_scalar { + $$ = NNEW(n_CLASS_MEMBER_DECLARATION); + $$->appendChild(NTYPE($3, n_VARIABLE)); + $$->appendChild($5); + + $$ = $1->appendChild($$); + } +| T_VARIABLE { + $$ = NNEW(n_CLASS_MEMBER_DECLARATION); + $$->appendChild(NTYPE($1, n_VARIABLE)); + $$->appendChild(NNEW(n_EMPTY)); + + $$ = NNEW(n_CLASS_MEMBER_DECLARATION_LIST)->appendChild($$); + } +| T_VARIABLE '=' static_scalar { + $$ = NNEW(n_CLASS_MEMBER_DECLARATION); + $$->appendChild(NTYPE($1, n_VARIABLE)); + $$->appendChild($3); + + $$ = NNEW(n_CLASS_MEMBER_DECLARATION_LIST)->appendChild($$); + } +; + +class_constant_declaration: + class_constant_declaration ',' T_STRING '=' static_scalar { + $$ = NNEW(n_CLASS_CONSTANT_DECLARATION); + $$->appendChild(NTYPE($3, n_STRING)); + $$->appendChild($5); + + $1->appendChild($$); + + $$ = $1; + } +| T_CONST T_STRING '=' static_scalar { + NTYPE($1, n_CLASS_CONSTANT_DECLARATION_LIST); + $$ = NNEW(n_CLASS_CONSTANT_DECLARATION); + $$->appendChild(NTYPE($2, n_STRING)); + $$->appendChild($4); + $1->appendChild($$); + + $$ = $1; + } +; + +echo_expr_list: + echo_expr_list ',' expr { + $1->appendChild($3); + } +| expr { + $$ = NNEW(n_ECHO_LIST); + $$->appendChild($1); + } +; + +for_expr: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| non_empty_for_expr +; + + +non_empty_for_expr: + non_empty_for_expr ',' expr { + $1->appendChild($3); + } +| expr { + $$ = NNEW(n_EXPRESSION_LIST); + $$->appendChild($1); + } +; + +expr_without_variable: + T_LIST '(' assignment_list ')' '=' expr { + NTYPE($1, n_LIST); + $1->appendChild(NEXPAND($2, $3, $4)); + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($5, n_OPERATOR)); + $$->appendChild($6); + } +| variable '=' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable '=' '&' variable { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + + NTYPE($3, n_VARIABLE_REFERENCE); + $3->appendChild($4); + + $$->appendChild($3); + } +| variable '=' '&' T_NEW class_name_reference ctor_arguments { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + + NTYPE($4, n_NEW); + $4->appendChild($5); + $4->appendChild($6); + + NTYPE($3, n_VARIABLE_REFERENCE); + $3->appendChild($4); + + $$->appendChild($3); + } +| T_NEW class_name_reference ctor_arguments { + NTYPE($1, n_NEW); + $1->appendChild($2); + $1->appendChild($3); + $$ = $1; + } +| T_CLONE expr { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| variable T_PLUS_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_MINUS_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_MUL_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_DIV_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_CONCAT_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_MOD_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_AND_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_OR_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_XOR_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_SL_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| variable T_SR_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| rw_variable T_INC { + $$ = NNEW(n_UNARY_POSTFIX_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + } +| T_INC rw_variable { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| rw_variable T_DEC { + $$ = NNEW(n_UNARY_POSTFIX_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + } +| T_DEC rw_variable { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| expr T_BOOLEAN_OR expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_BOOLEAN_AND expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_LOGICAL_OR expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_LOGICAL_AND expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_LOGICAL_XOR expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '|' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '&' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '^' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '.' expr { + + /* The concatenation operator generates n_CONCATENATION_LIST instead of + n_BINARY_EXPRESSION because we tend to run into stack depth issues in a + lot of real-world cases otherwise (e.g., in PHP and JSON decoders). */ + + if ($1->type == n_CONCATENATION_LIST && $3->type == n_CONCATENATION_LIST) { + $1->appendChild(NTYPE($2, n_OPERATOR)); + $1->appendChildren($3); + $$ = $1; + } else if ($1->type == n_CONCATENATION_LIST) { + $1->appendChild(NTYPE($2, n_OPERATOR)); + $1->appendChild($3); + $$ = $1; + } else if ($3->type == n_CONCATENATION_LIST) { + $$ = NNEW(n_CONCATENATION_LIST); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChildren($3); + } else { + $$ = NNEW(n_CONCATENATION_LIST); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } + } +| expr '+' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '-' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '*' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '/' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '%' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_SL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_SR expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| '+' expr %prec T_INC { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| '-' expr %prec T_INC { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| '!' expr { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| '~' expr { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| expr T_IS_IDENTICAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_IS_NOT_IDENTICAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_IS_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_IS_NOT_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '<' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_IS_SMALLER_OR_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr '>' expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_IS_GREATER_OR_EQUAL expr { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| expr T_INSTANCEOF class_name_reference { + $$ = NNEW(n_BINARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NTYPE($2, n_OPERATOR)); + $$->appendChild($3); + } +| '(' expr ')' { + NSPAN($1, n_PARENTHETICAL_EXPRESSION, $3); + $1->appendChild($2); + $$ = $1; + } +| expr '?' expr ':' expr { + $$ = NNEW(n_TERNARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild($3); + $$->appendChild($5); + } +| expr '?' ':' expr { + $$ = NNEW(n_TERNARY_EXPRESSION); + $$->appendChild($1); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild($4); + } +| internal_functions_in_yacc +| T_INT_CAST expr { + $$ = NNEW(n_CAST_EXPRESSION); + $$->appendChild(NTYPE($1, n_CAST)); + $$->appendChild($2); + } +| T_DOUBLE_CAST expr { + $$ = NNEW(n_CAST_EXPRESSION); + $$->appendChild(NTYPE($1, n_CAST)); + $$->appendChild($2); + } +| T_STRING_CAST expr { + $$ = NNEW(n_CAST_EXPRESSION); + $$->appendChild(NTYPE($1, n_CAST)); + $$->appendChild($2); + } +| T_UNICODE_CAST expr { + $$ = NNEW(n_CAST_EXPRESSION); + $$->appendChild(NTYPE($1, n_CAST)); + $$->appendChild($2); + } +| T_BINARY_CAST expr { + $$ = NNEW(n_CAST_EXPRESSION); + $$->appendChild(NTYPE($1, n_CAST)); + $$->appendChild($2); + } +| T_ARRAY_CAST expr { + $$ = NNEW(n_CAST_EXPRESSION); + $$->appendChild(NTYPE($1, n_CAST)); + $$->appendChild($2); + } +| T_OBJECT_CAST expr { + $$ = NNEW(n_CAST_EXPRESSION); + $$->appendChild(NTYPE($1, n_CAST)); + $$->appendChild($2); + } +| T_BOOL_CAST expr { + $$ = NNEW(n_CAST_EXPRESSION); + $$->appendChild(NTYPE($1, n_CAST)); + $$->appendChild($2); + } +| T_UNSET_CAST expr { + $$ = NNEW(n_CAST_EXPRESSION); + $$->appendChild(NTYPE($1, n_CAST)); + $$->appendChild($2); + } +| T_EXIT exit_expr { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| '@' expr { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| T_ARRAY '(' array_pair_list ')' { + NTYPE($1, n_ARRAY_LITERAL); + $1->appendChild($3); + NMORE($1, $4); + $$ = $1; + } +| T_BACKTICKS_EXPR { + NTYPE($1, n_BACKTICKS_EXPRESSION); + $$ = $1; + } +| scalar +| T_PRINT expr { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| function is_reference '(' parameter_list ')' lexical_vars '{' inner_statement_list '}' { + NSPAN($1, n_FUNCTION_DECLARATION, $9); + $1->appendChild(NNEW(n_EMPTY)); + $1->appendChild($2); + $1->appendChild(NNEW(n_EMPTY)); + $1->appendChild(NEXPAND($3, $4, $5)); + $$->appendChild($6); + $1->appendChild($8); + + $$ = $1; + } +| T_STATIC function is_reference '(' parameter_list ')' lexical_vars '{' inner_statement_list '}' { + NSPAN($2, n_FUNCTION_DECLARATION, $10); + NLMORE($2, $1); + + $$ = NNEW(n_FUNCTION_MODIFIER_LIST); + $$->appendChild(NTYPE($1, n_STRING)); + $2->appendChild($1); + + $2->appendChild(NNEW(n_EMPTY)); + $2->appendChild($3); + $2->appendChild(NNEW(n_EMPTY)); + $2->appendChild(NEXPAND($4, $5, $6)); + $2->appendChild($7); + $2->appendChild($9); + + $$ = $2; + } +; + +function: + T_FUNCTION +; + +lexical_vars: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| T_USE '(' lexical_var_list ')' { + NTYPE($1, n_LEXICAL_VARIABLE_LIST); + $1->appendChildren($3); + $$ = $1; + } +; + +lexical_var_list: + lexical_var_list ',' T_VARIABLE { + $$ = $1->appendChild(NTYPE($3, n_VARIABLE)); + } +| lexical_var_list ',' '&' T_VARIABLE { + NTYPE($3, n_VARIABLE_REFERENCE); + $3->appendChild(NTYPE($4, n_VARIABLE)); + $$ = $1->appendChild($3); + } +| T_VARIABLE { + $$ = NNEW(n_LEXICAL_VARIABLE_LIST); + $$->appendChild(NTYPE($1, n_VARIABLE)); + } +| '&' T_VARIABLE { + NTYPE($1, n_VARIABLE_REFERENCE); + $1->appendChild(NTYPE($2, n_VARIABLE)); + $$ = NNEW(n_LEXICAL_VARIABLE_LIST); + $$->appendChild($1); + } +; + +function_call: + namespace_name '(' function_call_parameter_list ')' { + $$ = NNEW(n_FUNCTION_CALL); + $$->appendChild($1); + $$->appendChild(NEXPAND($2, $3, $4)); + } +| T_NAMESPACE T_NS_SEPARATOR namespace_name '(' function_call_parameter_list ')' { + NLMORE($3, $1); + $$ = NNEW(n_FUNCTION_CALL); + $$->appendChild($3); + $$->appendChild(NEXPAND($4, $5, $6)); + } +| T_NS_SEPARATOR namespace_name '(' function_call_parameter_list ')' { + NLMORE($2, $1); + $$ = NNEW(n_FUNCTION_CALL); + $$->appendChild($2); + $$->appendChild(NEXPAND($3, $4, $5)); + } +| class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING '(' function_call_parameter_list ')' { + $$ = NNEW(n_CLASS_STATIC_ACCESS); + $$->appendChild($1); + $$->appendChild(NTYPE($3, n_STRING)); + + $$ = NNEW(n_FUNCTION_CALL)->appendChild($$); + $$->appendChild(NEXPAND($4, $5, $6)); + } +| variable_class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING '(' function_call_parameter_list ')' { + $$ = NNEW(n_CLASS_STATIC_ACCESS); + $$->appendChild($1); + $$->appendChild(NTYPE($3, n_STRING)); + + $$ = NNEW(n_FUNCTION_CALL)->appendChild($$); + $$->appendChild(NEXPAND($4, $5, $6)); + } +| variable_class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects '(' function_call_parameter_list ')' { + $$ = NNEW(n_CLASS_STATIC_ACCESS); + $$->appendChild($1); + $$->appendChild(NTYPE($3, n_STRING)); + + $$ = NNEW(n_FUNCTION_CALL)->appendChild($$); + $$->appendChild(NEXPAND($4, $5, $6)); + } +| class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects '(' function_call_parameter_list ')' { + $$ = NNEW(n_CLASS_STATIC_ACCESS); + $$->appendChild($1); + $$->appendChild(NTYPE($3, n_STRING)); + + $$ = NNEW(n_FUNCTION_CALL)->appendChild($$); + $$->appendChild(NEXPAND($4, $5, $6)); + } +| variable_without_objects '(' function_call_parameter_list ')' { + $$ = NNEW(n_FUNCTION_CALL); + $$->appendChild($1); + $$->appendChild(NEXPAND($2, $3, $4)); + } +; + +class_name: + T_STATIC { + $$ = NTYPE($1, n_CLASS_NAME); + } +| namespace_name { + $$ = NTYPE($1, n_CLASS_NAME); + } +| T_NAMESPACE T_NS_SEPARATOR namespace_name { + NLMORE($3, $1); + $$ = NTYPE($3, n_CLASS_NAME); + } +| T_NS_SEPARATOR namespace_name { + NLMORE($2, $1); + $$ = NTYPE($2, n_CLASS_NAME); + } +; + +fully_qualified_class_name: + namespace_name { + $$ = NTYPE($1, n_CLASS_NAME); + } +| T_NAMESPACE T_NS_SEPARATOR namespace_name { + NLMORE($3, $1); + $$ = NTYPE($3, n_CLASS_NAME); + } +| T_NS_SEPARATOR namespace_name { + NLMORE($2, $1); + $$ = NTYPE($2, n_CLASS_NAME); + } +; + +class_name_reference: + class_name +| dynamic_class_name_reference +; + +dynamic_class_name_reference: + base_variable T_OBJECT_OPERATOR object_property dynamic_class_name_variable_properties { + $$ = NNEW(n_OBJECT_PROPERTY_ACCESS); + $$->appendChild($1); + $$->appendChild($3); + for (xhpast::node_list_t::iterator ii = $4->children.begin(); ii != $4->children.end(); ++ii) { + $$ = NNEW(n_OBJECT_PROPERTY_ACCESS)->appendChild($$); + $$->appendChild(*ii); + } + } +| base_variable +; + +dynamic_class_name_variable_properties: + dynamic_class_name_variable_properties dynamic_class_name_variable_property { + $$ = $1->appendChild($2); + } +| /* empty */ { + $$ = NNEW(n_EMPTY); + } +; + +dynamic_class_name_variable_property: + T_OBJECT_OPERATOR object_property { + $$ = $2; + } +; + +exit_expr: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| '(' ')' { + NSPAN($1, n_EMPTY, $2); + $$ = $1; + } +| '(' expr ')' { + NSPAN($1, n_PARENTHETICAL_EXPRESSION, $3); + $1->appendChild($2); + $$ = $1; + } +; + +ctor_arguments: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| '(' function_call_parameter_list ')' { + $$ = NEXPAND($1, $2, $3); + } +; + +common_scalar: + T_LNUMBER { + $$ = NTYPE($1, n_NUMERIC_SCALAR); + } +| T_DNUMBER { + $$ = NTYPE($1, n_NUMERIC_SCALAR); + } +| T_CONSTANT_ENCAPSED_STRING { + $$ = NTYPE($1, n_STRING_SCALAR); + } +| T_LINE { + $$ = NTYPE($1, n_MAGIC_SCALAR); + } +| T_FILE { + $$ = NTYPE($1, n_MAGIC_SCALAR); + } +| T_DIR { + $$ = NTYPE($1, n_MAGIC_SCALAR); + } +| T_CLASS_C { + $$ = NTYPE($1, n_MAGIC_SCALAR); + } +| T_METHOD_C { + $$ = NTYPE($1, n_MAGIC_SCALAR); + } +| T_FUNC_C { + $$ = NTYPE($1, n_MAGIC_SCALAR); + } +| T_NS_C { + $$ = NTYPE($1, n_MAGIC_SCALAR); + } +| T_HEREDOC { + $$ = NTYPE($1, n_HEREDOC); + } +; + +static_scalar: /* compile-time evaluated scalars */ + common_scalar +| namespace_name +| T_NAMESPACE T_NS_SEPARATOR namespace_name { + NLMORE($3, $1); + $$ = $3; + } +| T_NS_SEPARATOR namespace_name { + NLMORE($2, $1); + $$ = $2; + } +| '+' static_scalar { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| '-' static_scalar { + $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); + $$->appendChild(NTYPE($1, n_OPERATOR)); + $$->appendChild($2); + } +| T_ARRAY '(' static_array_pair_list ')' { + NTYPE($1, n_ARRAY_LITERAL); + $1->appendChild($3); + NMORE($1, $4); + $$ = $1; + } +| static_class_constant +; + +static_class_constant: + class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING { + $$ = NNEW(n_CLASS_STATIC_ACCESS); + $$->appendChild($1); + $$->appendChild(NTYPE($3, n_STRING)); + } +; + +scalar: + T_STRING_VARNAME +| class_constant +| namespace_name +| T_NAMESPACE T_NS_SEPARATOR namespace_name { + $$ = NLMORE($3, $1); + } +| T_NS_SEPARATOR namespace_name { + $$ = NLMORE($2, $1); + } +| common_scalar +; + +static_array_pair_list: + /* empty */ { + $$ = NNEW(n_ARRAY_VALUE_LIST); + } +| non_empty_static_array_pair_list possible_comma { + $$ = NMORE($1, $2); + } +; + +possible_comma: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| ',' +; + +non_empty_static_array_pair_list: + non_empty_static_array_pair_list ',' static_scalar T_DOUBLE_ARROW static_scalar { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild($3); + $$->appendChild($5); + + $$ = $1->appendChild($$); + } +| non_empty_static_array_pair_list ',' static_scalar { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild($3); + + $$ = $1->appendChild($$); + } +| static_scalar T_DOUBLE_ARROW static_scalar { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild($1); + $$->appendChild($3); + } +| static_scalar { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild($1); + } +; + +expr: + r_variable +| expr_without_variable +; + +r_variable: + variable +; + +w_variable: + variable +; + +rw_variable: + variable +; + +variable: + base_variable_with_function_calls T_OBJECT_OPERATOR object_property method_or_not variable_properties { + $$ = NNEW(n_OBJECT_PROPERTY_ACCESS); + $$->appendChild($1); + $$->appendChild($3); + + if ($4->type != n_EMPTY) { + $$ = NNEW(n_METHOD_CALL)->appendChild($$); + $$->appendChild($4); + } + + for (xhpast::node_list_t::iterator ii = $5->children.begin(); ii != $5->children.end(); ++ii) { + if ((*ii)->type == n_CALL_PARAMETER_LIST) { + $$ = NNEW(n_METHOD_CALL)->appendChild($1); + $$->appendChild((*ii)); + } else { + $$ = NNEW(n_OBJECT_PROPERTY_ACCESS); + $$->appendChild((*ii)); + } + } + } +| base_variable_with_function_calls +; + +variable_properties: + variable_properties variable_property { + $$ = $1->appendChildren($2); + } +| /* empty */ { + $$ = NNEW(n_EMPTY); + } +; + +variable_property: + T_OBJECT_OPERATOR object_property method_or_not { + $$ = NNEW(n_EMPTY); + $$->appendChild($2); + if ($3->type != n_EMPTY) { + $$->appendChild($3); + } + } +; + +method_or_not: + '(' function_call_parameter_list ')' { + $$ = NEXPAND($1, $2, $3); + } +| /* empty */ { + $$ = NNEW(n_EMPTY); + } +; + +variable_without_objects: + reference_variable +| simple_indirect_reference reference_variable { + $$ = $1->appendChild($2); + } +; + +static_member: + class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects { + $$ = NNEW(n_CLASS_STATIC_ACCESS); + $$->appendChild($1); + $$->appendChild($3); + } +| variable_class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects { + $$ = NNEW(n_CLASS_STATIC_ACCESS); + $$->appendChild($1); + $$->appendChild($3); + } +; + +variable_class_name: + reference_variable +; + +base_variable_with_function_calls: + base_variable +| function_call +; + +base_variable: + reference_variable +| simple_indirect_reference reference_variable { + $$ = $1->appendChild($2); + } +| static_member +; + +reference_variable: + reference_variable '[' dim_offset ']' { + $$ = NNEW(n_INDEX_ACCESS); + $$->appendChild($1); + $$->appendChild($3); + NMORE($$, $4); + } +| reference_variable '{' expr '}' { + $$ = NNEW(n_INDEX_ACCESS); + $$->appendChild($1); + $$->appendChild($3); + NMORE($$, $4); + } +| compound_variable +; + +compound_variable: + T_VARIABLE { + NTYPE($1, n_VARIABLE); + } +| '$' '{' expr '}' { + NSPAN($1, n_VARIABLE_EXPRESSION, $4); + $1->appendChild($3); + $$ = $1; + } +; + +dim_offset: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| expr { + $$ = $1; + } +; + +object_property: + object_dim_list +| variable_without_objects +; + +object_dim_list: + object_dim_list '[' dim_offset ']' { + $$ = NNEW(n_INDEX_ACCESS); + $$->appendChild($1); + $$->appendChild($3); + NMORE($$, $4) + } +| object_dim_list '{' expr '}' { + $$ = NNEW(n_INDEX_ACCESS); + $$->appendChild($1); + $$->appendChild($3); + NMORE($$, $4); + } +| variable_name +; + +variable_name: + T_STRING { + NTYPE($1, n_STRING); + $$ = $1; + } +| '{' expr '}' { + $$ = NEXPAND($1, $2, $3); + } +; + +simple_indirect_reference: + '$' { + $$ = NTYPE($1, n_VARIABLE_VARIABLE); + } +| simple_indirect_reference '$' { + $$ = NMORE($1, $2); + } +; + +assignment_list: + assignment_list ',' assignment_list_element { + $$ = $1->appendChild($3); + } +| assignment_list_element { + $$ = NNEW(n_ASSIGNMENT_LIST); + $$->appendChild($1); + } +; + +assignment_list_element: + variable +| T_LIST '(' assignment_list ')' { + $$ = NNEW(n_LIST); + $$->appendChild($3); + NMORE($$, $4); + } +| /* empty */ { + $$ = NNEW(n_EMPTY); + } +; + +array_pair_list: + /* empty */ { + $$ = NNEW(n_ARRAY_VALUE_LIST); + } +| non_empty_array_pair_list possible_comma { + $$ = NMORE($1, $2); + } +; + +non_empty_array_pair_list: + non_empty_array_pair_list ',' expr T_DOUBLE_ARROW expr { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild($3); + $$->appendChild($5); + + $$ = $1->appendChild($$); + } +| non_empty_array_pair_list ',' expr { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild($3); + + $$ = $1->appendChild($$); + } +| expr T_DOUBLE_ARROW expr { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild($1); + $$->appendChild($3); + + $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); + } +| expr { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild($1); + + $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); + } +| non_empty_array_pair_list ',' expr T_DOUBLE_ARROW '&' w_variable { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild($3); + $$->appendChild(NTYPE($5, n_VARIABLE_REFERENCE)->appendChild($6)); + + $$ = $1->appendChild($$); + } +| non_empty_array_pair_list ',' '&' w_variable { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild(NTYPE($3, n_VARIABLE_REFERENCE)->appendChild($4)); + + $$ = $1->appendChild($$); + } +| expr T_DOUBLE_ARROW '&' w_variable { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild($1); + $$->appendChild(NTYPE($3, n_VARIABLE_REFERENCE)->appendChild($4)); + + $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); + } +| '&' w_variable { + $$ = NNEW(n_ARRAY_VALUE); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild(NTYPE($1, n_VARIABLE_REFERENCE)->appendChild($2)); + + $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); + } +; + +internal_functions_in_yacc: + T_ISSET '(' isset_variables ')' { + NTYPE($1, n_SYMBOL_NAME); + + NSPAN($2, n_CALL_PARAMETER_LIST, $4); + $2->appendChildren($3); + + $$ = NNEW(n_FUNCTION_CALL); + $$->appendChild($1); + $$->appendChild($2); + } +| T_EMPTY '(' variable ')' { + NTYPE($1, n_SYMBOL_NAME); + + NSPAN($2, n_CALL_PARAMETER_LIST, $4); + $2->appendChild($3); + + $$ = NNEW(n_FUNCTION_CALL); + $$->appendChild($1); + $$->appendChild($2); + } +| T_INCLUDE expr { + $$ = NTYPE($1, n_INCLUDE_FILE)->appendChild($2); + } +| T_INCLUDE_ONCE expr { + $$ = NTYPE($1, n_INCLUDE_FILE)->appendChild($2); + } +| T_EVAL '(' expr ')' { + NTYPE($1, n_SYMBOL_NAME); + + NSPAN($2, n_CALL_PARAMETER_LIST, $4); + $2->appendChild($3); + + $$ = NNEW(n_FUNCTION_CALL); + $$->appendChild($1); + $$->appendChild($2); + } +| T_REQUIRE expr { + $$ = NTYPE($1, n_INCLUDE_FILE)->appendChild($2); + } +| T_REQUIRE_ONCE expr { + $$ = NTYPE($1, n_INCLUDE_FILE)->appendChild($2); + } +; + +isset_variables: + variable { + $$ = NNEW(n_EMPTY); + $$->appendChild($1); + } +| isset_variables ',' variable { + $$ = $1->appendChild($3); + } +; + +class_constant: + class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING { + $$ = NNEW(n_CLASS_STATIC_ACCESS); + $$->appendChild($1); + $$->appendChild(NTYPE($3, n_STRING)); + } +| variable_class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING { + $$ = NNEW(n_CLASS_STATIC_ACCESS); + $$->appendChild($1); + $$->appendChild(NTYPE($3, n_STRING)); + } +; + +// +// XHP Extensions + +// Tags +expr_without_variable: + xhp_tag_expression { + yyextra->used = true; + $$ = $1; + } +; + +xhp_tag_expression: + xhp_singleton +| xhp_tag_open xhp_children xhp_tag_close { + $$ = NNEW(n_XHP_TAG); + $$->appendChild($1); + $$->appendChild($2); + $$->appendChild($3); + } +; + +xhp_singleton: + xhp_tag_start xhp_attributes '/' '>' { + pop_state(); // XHP_ATTRS + + $1->appendChild($2); + NMORE($$, $4); + + $$ = NNEW(n_XHP_TAG)->appendChild($1); + $$->appendChild(NNEW(n_EMPTY)); + $$->appendChild(NNEW(n_EMPTY)); + } +; + +xhp_tag_open: + xhp_tag_start xhp_attributes '>' { + pop_state(); // XHP_ATTRS + push_state(XHP_CHILD_START); +/* TODO: RESTORE THIS + yyextra->pushTag((*($1->l_tok))->value.c_str()); +*/ + yyextra->pushTag("TODO"); + + $$ = $1->appendChild($2); + NMORE($1, $3); + } +; + +xhp_tag_close: + T_XHP_LT_DIV xhp_label_no_space '>' { + pop_state(); // XHP_CHILD_START +/* TOOD: RESTORE THIS + if (yyextra->peekTag() != (*($2->l_tok))->value.c_str()) { + string e1 = (*($2->l_tok))->value.c_str(); + string e2 = yyextra->peekTag(); + replacestr(e1, "__", ":"); + replacestr(e1, "_", "-"); + replacestr(e2, "__", ":"); + replacestr(e2, "_", "-"); + string e = "syntax error, mismatched tag , expecting "; + yyerror(yyscanner, NULL, e.c_str()); + yyextra->terminated = true; + } +*/ + yyextra->popTag(); + if (yyextra->haveTag()) { + set_state(XHP_CHILD_START); + } + + $$ = NSPAN($1, n_XHP_TAG_CLOSE, $3); + } +| T_XHP_LT_DIV_GT { + // empty end tag -- SGML SHORTTAG + pop_state(); // XHP_CHILD_START + yyextra->popTag(); + if (yyextra->haveTag()) { + set_state(XHP_CHILD_START); + } + $$ = NTYPE($1, n_XHP_TAG_CLOSE); + } +; + +xhp_tag_start: + '<' xhp_label_immediate { + $$ = NTYPE($1, n_XHP_TAG_OPEN); + $$->appendChild($2); + } +; + +// Children +xhp_literal_text: + T_XHP_TEXT { + $$ = NTYPE($1, n_XHP_TEXT); + } +| T_XHP_ENTITY { + $$ = NTYPE($1, n_XHP_ENTITY); + } +| xhp_literal_text T_XHP_TEXT { + $$ = NMORE($1, $2); + } +| xhp_literal_text T_XHP_ENTITY { + $$ = NMORE($1, $2); + } +; + +xhp_children: + /* empty */ { + $$ = NNEW(n_XHP_NODE_LIST); + } +| xhp_literal_text { + set_state(XHP_CHILD_START); + $$ = NNEW(n_XHP_NODE_LIST)->appendChild($1); + } +| xhp_children xhp_child { + set_state(XHP_CHILD_START); + $$ = $1->appendChild($2); + } +| xhp_children xhp_child xhp_literal_text { + set_state(XHP_CHILD_START); + $$ = $1->appendChild($2)->appendChild($3); + } +; + +xhp_child: + xhp_tag_expression +| '{' { + push_state(PHP); + yyextra->pushStack(); + } expr '}' { + pop_state(); + yyextra->popStack(); + } { + set_state(XHP_CHILD_START); + $$ = NNEW(n_XHP_EXPRESSION); + $$->appendChild($3); + NEXPAND($1, $$, $4); + } +; + +// Attributes +xhp_attributes: + /* empty */ { + push_state(XHP_ATTRS); + $$ = NNEW(n_XHP_ATTRIBUTE_LIST); + } +| xhp_attributes xhp_attribute { + $$ = $1->appendChild($2); + } +; + +xhp_attribute: + xhp_label_pass '=' xhp_attribute_value { + $$ = NNEW(n_XHP_ATTRIBUTE); + $$->appendChild($1); + $$->appendChild($3); + } +; + +xhp_attribute_value: + '"' { push_state(XHP_ATTR_VAL); } xhp_attribute_quoted_value '"' { + $$ = NSPAN($1, n_XHP_ATTRIBUTE_LITERAL, $4); + $$->appendChild($3); + } +| '{' { push_state(PHP); } expr { pop_state(); } '}' { + $$ = NSPAN($1, n_XHP_ATTRIBUTE_EXPRESSION, $5); + $$->appendChild($3); + } +; + +xhp_attribute_quoted_value: + /* empty */ { + $$ = NNEW(n_EMPTY); + } +| xhp_literal_text { + $$ = NTYPE($1, n_XHP_LITERAL); + } +; + +// Misc +xhp_label_immediate: + { push_state(XHP_LABEL); } xhp_label_ xhp_whitespace_hack { + pop_state(); + $$ = $2; + } +; + +xhp_label_no_space: + { push_state(XHP_LABEL); } xhp_label_ { + pop_state(); + $$ = $2; + } +; + +xhp_label_pass: + { push_state(XHP_LABEL_WHITESPACE); } xhp_label_pass_ xhp_whitespace_hack { + pop_state(); + $$ = $2; + } +; + +xhp_label_pass_immediate: + { push_state(XHP_LABEL); } xhp_label_pass_ xhp_whitespace_hack { + pop_state(); + $$ = $2; + } +; + +xhp_label: + { push_state(XHP_LABEL_WHITESPACE); } xhp_label_ xhp_whitespace_hack { + pop_state(); + $$ = $2; + } +; + +xhp_label_: + T_STRING { + // XHP_LABEL is popped in the scanner on " ", ">", "/", or "=" + push_state(XHP_LABEL); + $$ = NTYPE($1, n_XHP_LITERAL); + } +| xhp_label_ T_XHP_COLON T_STRING { + $$ = NMORE($1, $3); + } +| xhp_label_ T_XHP_HYPHEN T_STRING { + $$ = NMORE($1, $3); + } +; + +xhp_label_pass_: + T_STRING { + // XHP_LABEL is popped in the scanner on " ", ">", "/", or "=" + push_state(XHP_LABEL); + $$ = NTYPE($1, n_XHP_LITERAL); + } +| xhp_label_pass_ T_XHP_COLON T_STRING { + $$ = NMORE($1, $3); + } +| xhp_label_pass_ T_XHP_HYPHEN T_STRING { + $$ = NMORE($1, $3); + } +; + +xhp_whitespace_hack: + T_XHP_WHITESPACE +| /* empty */ +; + +// Elements +class_declaration_statement: + class_entry_type ':' xhp_label_immediate extends_from implements_list '{' { + yyextra->expecting_xhp_class_statements = true; + yyextra->used_attributes = false; + } class_statement_list { + yyextra->expecting_xhp_class_statements = false; + } '}' { + $$ = NNEW(n_CLASS_DECLARATION); + $$->appendChild($1); + $$->appendChild(NSPAN($2, n_STRING, $3)); + $$->appendChild($4); + $$->appendChild($5); + $$->appendChild($8); + NMORE($$, $10); + + $$ = NNEW(n_STATEMENT)->appendChild($$); + + +//! $$ = $1 + " xhp_" + $3 + $4 + $5 + $6 + $8; +//! if (yyextra->used_attributes) { +//! $$ = $$ + +//! "protected static function &__xhpAttributeDeclaration() {" + +//! "static $_ = -1;" + +//! "if ($_ === -1) {" + +//! "$_ = array_merge(parent::__xhpAttributeDeclaration(), " + +//! yyextra->attribute_inherit + +//! "array(" + yyextra->attribute_decls + "));" + +//! "}" + +//! "return $_;" +//! "}"; +//! } +//! $$ = $$ + $10; + yyextra->used = true; + } +; + +// Element attribute declaration +class_statement: + T_XHP_ATTRIBUTE { push_state(XHP_ATTR_TYPE_DECL); } xhp_attribute_decls ';' { + pop_state(); + yyextra->used = true; + yyextra->used_attributes = true; +//! $$ = ""; // this will be injected when the class closes + } +; + +xhp_attribute_decls: + xhp_attribute_decl {} +| xhp_attribute_decls ',' xhp_attribute_decl {} +; + +xhp_attribute_decl: + xhp_attribute_decl_type xhp_label_pass xhp_attribute_default xhp_attribute_is_required { +//! $2.strip_lines(); +//! yyextra->attribute_decls = yyextra->attribute_decls + +//! "'" + $2 + "'=>array(" + $1 + "," + $3 + ", " + $4 + ")," + } +| T_XHP_COLON xhp_label_immediate { +//! $2.strip_lines(); +//! yyextra->attribute_inherit = yyextra->attribute_inherit + +//! "xhp_" + $2 + "::__xhpAttributeDeclaration(),"; + } +; + +xhp_attribute_decl_type: + T_XHP_STRING { +//! $$ = "1, null"; + } +| T_XHP_BOOLEAN { +//! $$ = "2, null"; + } +| T_XHP_NUMBER { +//! $$ = "3, null"; + } +| T_XHP_ARRAY { +//! $$ = "4, null"; + } +| class_name { +//! $$ = "5, '" + $1 + "'"; + } +| T_VAR { +//! $$ = "6, null"; + } +| T_XHP_ENUM '{' { push_state(PHP); } xhp_attribute_enum { pop_state(); } '}' { +//! $$ = "7, array(" + $4 + ")"; + } +| T_XHP_FLOAT { +//! ... +} +; + +xhp_attribute_enum: + common_scalar { +//! $1.strip_lines(); +//! $$ = $1; + } +| xhp_attribute_enum ',' common_scalar { +//! $3.strip_lines(); +//! $$ = $1 + ", " + $3; + } +; + +xhp_attribute_default: + '=' common_scalar { +//! $2.strip_lines(); +//! $$ = $2; + } +| '=' T_STRING { +//! $2.strip_lines(); +//! $$ = $2; + } +| /* empty */ { +//! $$ = "null"; + } +; + +xhp_attribute_is_required: + T_XHP_REQUIRED { +//! $$ = "1"; + } +| /* empty */ { +//! $$ = "0"; + } +; + +// Element category declaration +class_statement: + T_XHP_CATEGORY { push_state(PHP_NO_RESERVED_WORDS_PERSIST); } xhp_category_list ';' { + pop_state(); + yyextra->used = true; +//! $$ = +//! "protected function &__xhpCategoryDeclaration() { \ !!! +//! static $_ = array(" + $3 + ");" + +//! "return $_;" + +//! "}"; + } +; + +xhp_category_list: + '%' xhp_label_pass_immediate { +//! $$ = "'" + $2 + "' => 1"; + } +| xhp_category_list ',' '%' xhp_label_pass_immediate { +//! $$ = $1 + ",'" + $4 + "' => 1"; + } +; + +// Element child list +class_statement: + T_XHP_CHILDREN { push_state(XHP_CHILDREN_DECL); } xhp_children_decl ';' { + // XHP_CHILDREN_DECL is popped in the scanner on ';' + yyextra->used = true; +//! $$ = "protected function &__xhpChildrenDeclaration() {" + $3 + "}"; + } +; + +xhp_children_decl: + xhp_children_paren_expr { +//! $$ = "static $_ = " + $1 + "; return $_;"; + } +| T_XHP_ANY { +//! $$ = "static $_ = 1; return $_;"; + } +| T_XHP_EMPTY { +//! $$ = "static $_ = 0; return $_;"; + } +; + +xhp_children_paren_expr: + '(' xhp_children_decl_expr ')' { +//! $$ = "array(0, 5, " + $2 + ")"; + } +| '(' xhp_children_decl_expr ')' '*' { +//! $$ = "array(1, 5, " + $2 + ")"; + } +| '(' xhp_children_decl_expr ')' '?' { +//! $$ = "array(2, 5, " + $2 + ")"; + } +| '(' xhp_children_decl_expr ')' '+' { +//! $$ = "array(3, 5, " + $2 + ")"; + } +; + +xhp_children_decl_expr: + xhp_children_paren_expr +| xhp_children_decl_tag { +//! $$ = "array(0, " + $1 + ")"; + } +| xhp_children_decl_tag '*' { +//! $$ = "array(1, " + $1 + ")"; + } +| xhp_children_decl_tag '?' { +//! $$ = "array(2, " + $1 + ")"; + } +| xhp_children_decl_tag '+' { +//! $$ = "array(3, " + $1 + ")"; + } +| xhp_children_decl_expr ',' xhp_children_decl_expr { +//! $$ = "array(4, " + $1 + "," + $3 + ")" + } +| xhp_children_decl_expr '|' xhp_children_decl_expr { +//! $$ = "array(5, " + $1 + "," + $3 + ")" + } +; + +xhp_children_decl_tag: + T_XHP_ANY { +//! $$ = "1, null"; + } +| T_XHP_PCDATA { +//! $$ = "2, null"; + } +| T_XHP_COLON xhp_label { +//! $$ = "3, \'xhp_" + $2 + "\'"; + } +| '%' xhp_label { +//! $$ = "4, \'" + $2 + "\'"; + } +; + +// Make XHP classes usable anywhere you see a real class +class_name: + T_XHP_COLON xhp_label_immediate { + pop_state(); + push_state(PHP); + yyextra->used = true; +//! $$ = "xhp_" + $2; + } +; + +fully_qualified_class_name: + T_XHP_COLON xhp_label_immediate { + pop_state(); + push_state(PHP); + yyextra->used = true; +//! $$ = "xhp_" + $2; + } +; + +// Fix the "bug" in PHP's grammar where you can't chain the [] operator on a +// function call. +// This introduces some shift/reduce conflicts. We want the shift here to fall +// back to regular PHP grammar. In the case where it's an extension of the PHP +// grammar our code gets picked up. +expr_without_variable: + expr '[' dim_offset ']' { + if (yyextra->idx_expr) { + yyextra->used = true; + } + $$ = NNEW(n_INDEX_ACCESS); + $$->appendChild($1); + $$->appendChild($3); + NMORE($$, $4); + } +; + + +%% + +const char* yytokname(int tok) { + if (tok < 255) { + return NULL; + } + return yytname[YYTRANSLATE(tok)]; +} diff --git a/support/xhpast/parser_nodes.php b/support/xhpast/parser_nodes.php new file mode 100755 index 00000000..699ebd15 --- /dev/null +++ b/support/xhpast/parser_nodes.php @@ -0,0 +1,123 @@ + 'n_PROGRAM', + 9001 => 'n_SYMBOL_NAME', + 9002 => 'n_HALT_COMPILER', + 9003 => 'n_NAMESPACE', + 9004 => 'n_STATEMENT', + 9005 => 'n_EMPTY', + 9006 => 'n_STATEMENT_LIST', + 9007 => 'n_OPEN_TAG', + 9008 => 'n_CLOSE_TAG', + 9009 => 'n_USE_LIST', + 9010 => 'n_USE', + 9011 => 'n_CONSTANT_DECLARATION_LIST', + 9012 => 'n_CONSTANT_DECLARATION', + 9013 => 'n_STRING', + 9014 => 'n_LABEL', + 9015 => 'n_CONDITION_LIST', + 9016 => 'n_CONTROL_CONDITION', + 9017 => 'n_IF', + 9018 => 'n_ELSEIF', + 9019 => 'n_ELSE', + 9020 => 'n_WHILE', + 9021 => 'n_DO_WHILE', + 9022 => 'n_FOR', + 9023 => 'n_FOR_EXPRESSION', + 9024 => 'n_SWITCH', + 9025 => 'n_BREAK', + 9026 => 'n_CONTINUE', + 9027 => 'n_RETURN', + 9028 => 'n_GLOBAL_DECLARATION_LIST', + 9029 => 'n_GLOBAL_DECLARATION', + 9030 => 'n_STATIC_DECLARATION_LIST', + 9031 => 'n_STATIC_DECLARATION', + 9032 => 'n_ECHO_LIST', + 9033 => 'n_ECHO', + 9034 => 'n_INLINE_HTML', + 9035 => 'n_UNSET_LIST', + 9036 => 'n_UNSET', + 9037 => 'n_FOREACH', + 9038 => 'n_FOREACH_EXPRESSION', + 9039 => 'n_THROW', + 9040 => 'n_GOTO', + 9041 => 'n_TRY', + 9042 => 'n_CATCH_LIST', + 9043 => 'n_CATCH', + 9044 => 'n_DECLARE', + 9045 => 'n_DECLARE_DECLARATION_LIST', + 9046 => 'n_DECLARE_DECLARATION', + 9047 => 'n_VARIABLE', + 9048 => 'n_REFERENCE', + 9049 => 'n_VARIABLE_REFERENCE', + 9050 => 'n_FUNCTION_DECLARATION', + 9051 => 'n_CLASS_DECLARATION', + 9052 => 'n_CLASS_ATTRIBUTES', + 9053 => 'n_EXTENDS', + 9054 => 'n_EXTENDS_LIST', + 9055 => 'n_IMPLEMENTS_LIST', + 9056 => 'n_INTERFACE_DECLARATION', + 9057 => 'n_CASE', + 9058 => 'n_DEFAULT', + 9059 => 'n_DECLARATION_PARAMETER_LIST', + 9060 => 'n_DECLARATION_PARAMETER', + 9061 => 'n_TYPE_NAME', + 9062 => 'n_VARIABLE_VARIABLE', + 9063 => 'n_CLASS_MEMBER_DECLARATION_LIST', + 9064 => 'n_CLASS_MEMBER_DECLARATION', + 9065 => 'n_CLASS_CONSTANT_DECLARATION_LIST', + 9066 => 'n_CLASS_CONSTANT_DECLARATION', + 9067 => 'n_METHOD_DECLARATION', + 9068 => 'n_METHOD_MODIFIER_LIST', + 9069 => 'n_FUNCTION_MODIFIER_LIST', + 9070 => 'n_CLASS_MEMBER_MODIFIER_LIST', + 9071 => 'n_EXPRESSION_LIST', + 9072 => 'n_LIST', + 9073 => 'n_ASSIGNMENT', + 9074 => 'n_NEW', + 9075 => 'n_UNARY_PREFIX_EXPRESSION', + 9076 => 'n_UNARY_POSTFIX_EXPRESSION', + 9077 => 'n_BINARY_EXPRESSION', + 9078 => 'n_TERNARY_EXPRESSION', + 9079 => 'n_CAST_EXPRESSION', + 9080 => 'n_CAST', + 9081 => 'n_OPERATOR', + 9082 => 'n_ARRAY_LITERAL', + 9083 => 'n_EXIT_EXPRESSION', + 9084 => 'n_BACKTICKS_EXPRESSION', + 9085 => 'n_LEXICAL_VARIABLE_LIST', + 9086 => 'n_NUMERIC_SCALAR', + 9087 => 'n_STRING_SCALAR', + 9088 => 'n_MAGIC_SCALAR', + 9089 => 'n_CLASS_STATIC_ACCESS', + 9090 => 'n_CLASS_NAME', + 9091 => 'n_MAGIC_CLASS_KEYWORD', + 9092 => 'n_OBJECT_PROPERTY_ACCESS', + 9093 => 'n_ARRAY_VALUE_LIST', + 9094 => 'n_ARRAY_VALUE', + 9095 => 'n_CALL_PARAMETER_LIST', + 9096 => 'n_VARIABLE_EXPRESSION', + 9097 => 'n_INCLUDE_FILE', + 9098 => 'n_HEREDOC', + 9099 => 'n_FUNCTION_CALL', + 9100 => 'n_INDEX_ACCESS', + 9101 => 'n_ASSIGNMENT_LIST', + 9102 => 'n_METHOD_CALL', + 9103 => 'n_XHP_TAG', + 9104 => 'n_XHP_TAG_OPEN', + 9105 => 'n_XHP_TAG_CLOSE', + 9106 => 'n_XHP_TEXT', + 9107 => 'n_XHP_EXPRESSION', + 9108 => 'n_XHP_ATTRIBUTE_LIST', + 9109 => 'n_XHP_ATTRIBUTE', + 9110 => 'n_XHP_LITERAL', + 9111 => 'n_XHP_ATTRIBUTE_LITERAL', + 9112 => 'n_XHP_ATTRIBUTE_EXPRESSION', + 9113 => 'n_XHP_NODE_LIST', + 9114 => 'n_XHP_ENTITY', + 9115 => 'n_CONCATENATION_LIST', + 9116 => 'n_PARENTHETICAL_EXPRESSION', + ); +} diff --git a/support/xhpast/scanner.l b/support/xhpast/scanner.l new file mode 100755 index 00000000..222f3c33 --- /dev/null +++ b/support/xhpast/scanner.l @@ -0,0 +1,1085 @@ +/* + * Copyright 2011 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +%{ +#include "ast.hpp" +#define push_state(s) xhp_new_push_state(s, yyg) +#define pop_state() xhp_new_pop_state(yyg) +#define set_state(s) xhp_set_state(s, yyg) +#define last_token() yyextra->last_token + +#define YY_USER_ACTION \ + if (!yyg->yy_more_len) \ + yyextra->first_lineno = yyextra->lineno; +#define pttok(t, txt) \ + yyextra->token_list.push_back(new xhpast::Token(t, txt, yyextra->list_size++)); \ + *yylval = new xhpast::Node(0, yyextra->list_size - 1); +#define ptok(t) \ + pttok(t, yytext); +#define tok(t) \ + ptok(t); \ + return yy_token(t, yyg) +#define tokt(t) pttok(T_XHP_ENTITY, yytext); push_state(XHP_AFTER_ENT); return yyextra->last_token = T_XHP_ENTITY; +#define YY_USER_INIT \ + if (yyextra->insert_token) { \ + yyg->yy_init = 0; \ + int ft = yyextra->insert_token; \ + yyextra->insert_token = 0; \ + return yy_token(ft, yyg); \ + } + +using namespace std; + +const char* yytokname(int tok); +static int yy_token(int tok, struct yyguts_t* yyg); +static void yy_scan_newlines(const char* text, struct yyguts_t* yyg); + +static bool utf8ize(uint32_t v, char* buf /* [5] */) { + if (v <= 0x7f) { // 0xxxxxxx + buf[0] = v; + buf[1] = 0; + } else if (v <= 0x7ff) { // 110yyyxx 10xxxxxx + buf[0] = 0xc0 | (v >> 6); + buf[1] = 0x80 | (v & 0x3f); + buf[2] = 0; + } else if (v <= 0xffff) { // 1110yyyy 10yyyyxx 10xxxxxx + buf[0] = 0xe0 | (v >> 12); + buf[1] = 0x80 | ((v >> 6) & 0x3f); + buf[2] = 0x80 | (v & 0x3f); + buf[3] = 0; + } else if (v <= 0x1fffff) { // 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx + buf[0] = 0xf0 | (v >> 18); + buf[1] = 0x80 | ((v >> 12) & 0x3f); + buf[2] = 0x80 | ((v >> 6) & 0x3f); + buf[3] = 0x80 | (v & 0x3f); + buf[4] = 0; + } else { + return false; + } + return true; +} + +%} + +%option prefix="xhpast" +%option reentrant + /* PHP allows IF or if */ +%option case-insensitive +%option noyywrap nodefault +%option stack +%option bison-bridge +%option 8bit + + /* I think an interactive scanner is required because of the bison state + * pushing we do. I'm putting an explicit interactive declaration here in case + * someone tries adding -CF or whatever to the make flags. */ +%option interactive + + /* The different lexing states. Note that the transitions are done either + * in the lex actions, or in a generic manner in yy_token(). */ +%s PHP +%s PHP_COMMENT +%s PHP_EOL_COMMENT +%s PHP_DOC_COMMENT +%s PHP_HEREDOC_START +%s PHP_HEREDOC_NSTART +%s PHP_HEREDOC_NEWLINE +%s PHP_HEREDOC_DATA +%s PHP_NO_RESERVED_WORDS +%s PHP_NO_RESERVED_WORDS_PERSIST +%s PHP_ +%s XHP_LABEL +%s XHP_LABEL_WHITESPACE +%s XHP_ATTRS +%s XHP_ATTR_VAL +%s XHP_AFTER_ENT +%s XHP_CHILD +%s XHP_CHILD_START +%s XHP_INVALID_ENTITY +%s XHP_ATTR_TYPE_DECL +%s XHP_CHILDREN_DECL + +LNUM [0-9]+ +DNUM ([0-9]*"."[0-9]+)|([0-9]+"."[0-9]*) +EXPONENT_DNUM (({LNUM}|{DNUM})[eE][+-]?{LNUM}) +HNUM "0x"[0-9a-fA-F]+ + +LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]* +BYTE (.|\n) + +WHITESPACE [ \n\r\t]+ +TABS_AND_SPACES [ \t]* +NEWLINE ("\r\n"|"\n"|"\r") + +%% + +{ + "bool" tok(T_XHP_BOOLEAN); + "int" tok(T_XHP_NUMBER); + "float" tok(T_XHP_FLOAT); + "var" tok(T_VAR); + "array" tok(T_XHP_ARRAY); + "string" tok(T_XHP_STRING); + "enum" tok(T_XHP_ENUM); + @required tok(T_XHP_REQUIRED); + "(" tok('('); + ":" tok(T_XHP_COLON); +} + + /* Open / close PHP + inline HTML */ +{ + "short_tags) { + tok(T_OPEN_TAG); + } else { + tok(T_INLINE_HTML); + } + } + "short_tags) { + tok(T_OPEN_TAG_WITH_ECHO); + } else { + tok(T_INLINE_HTML); + } + } + "<%" { + if (yyextra->asp_tags) { + tok(T_OPEN_TAG); + } else { + tok(T_INLINE_HTML); + } + } + "<%=" { + if (yyextra->asp_tags) { + tok(T_OPEN_TAG_WITH_ECHO); + } else { + tok(T_INLINE_HTML); + } + } + "<"|[^<]* { + yy_scan_newlines(yytext, yyg); + tok(T_INLINE_HTML); + } +} +{ + ("?>"|""){NEWLINE}? { + yy_scan_newlines(yytext + 2, yyg); + tok(T_CLOSE_TAG); + } + "%>" { + if (yyextra->asp_tags) { + tok(T_CLOSE_TAG); + } else { + yyless(1); + tok(yytext[0]); + } + } +} + + /* Comments and whitespace */ +{ + "#"|"//" { + push_state(PHP_EOL_COMMENT); + yymore(); + } + "/**"{WHITESPACE} { + yy_scan_newlines(yytext + 3, yyg); + push_state(PHP_DOC_COMMENT); + yymore(); + } + "/*" { + push_state(PHP_COMMENT); + yymore(); + } + {WHITESPACE}+ { + yy_scan_newlines(yytext, yyg); + ptok(T_WHITESPACE); + } +} +{ + {NEWLINE} { + ++yyextra->lineno; + ptok(T_COMMENT); + pop_state(); + } + [^\r\n?]+ yymore(); + "?>" { + yyless(yyleng - 2); + ptok(T_COMMENT); + pop_state(); + } + . yymore(); +} +{ + {NEWLINE} { + ++yyextra->lineno; + yymore(); + } + [^*\r\n]+|"*" yymore(); +} +"*/" { + ptok(T_DOC_COMMENT); + pop_state(); +} +"*/" { + ptok(T_COMMENT); + pop_state(); +} + + /* Reserved words */ +{ + include tok(T_INCLUDE); + include_once tok(T_INCLUDE_ONCE); + eval tok(T_EVAL); + require tok(T_REQUIRE); + require_once tok(T_REQUIRE_ONCE); + or tok(T_LOGICAL_OR); + xor tok(T_LOGICAL_XOR); + and tok(T_LOGICAL_AND); + print tok(T_PRINT); + instanceof tok(T_INSTANCEOF); + new tok(T_NEW); + clone tok(T_CLONE); + exit tok(T_EXIT); + if tok(T_IF); + elseif tok(T_ELSEIF); + else tok(T_ELSE); + endif tok(T_ENDIF); + echo tok(T_ECHO); + do tok(T_DO); + while tok(T_WHILE); + endwhile tok(T_ENDWHILE); + for tok(T_FOR); + endfor tok(T_ENDFOR); + foreach tok(T_FOREACH); + endforeach tok(T_ENDFOREACH); + declare tok(T_DECLARE); + enddeclare tok(T_ENDDECLARE); + as tok(T_AS); + switch tok(T_SWITCH); + endswitch tok(T_ENDSWITCH); + case tok(T_CASE); + default tok(T_DEFAULT); + break tok(T_BREAK); + continue tok(T_CONTINUE); + goto tok(T_GOTO); + function tok(T_FUNCTION); + const tok(T_CONST); + return tok(T_RETURN); + try tok(T_TRY); + catch tok(T_CATCH); + throw tok(T_THROW); + use tok(T_USE); + global tok(T_GLOBAL); + static tok(T_STATIC); + abstract tok(T_ABSTRACT); + final tok(T_FINAL); + private tok(T_PRIVATE); + protected tok(T_PROTECTED); + public tok(T_PUBLIC); + var tok(T_VAR); + unset tok(T_UNSET); + isset tok(T_ISSET); + empty tok(T_EMPTY); + __halt_compiler tok(T_HALT_COMPILER); + class tok(T_CLASS); + interface tok(T_INTERFACE); + extends tok(T_EXTENDS); + implements tok(T_IMPLEMENTS); + list tok(T_LIST); + array tok(T_ARRAY); + __class__ tok(T_CLASS_C); + __method__ tok(T_METHOD_C); + __function__ tok(T_FUNC_C); + __line__ tok(T_LINE); + __file__ tok(T_FILE); + namespace tok(T_NAMESPACE); + __namespace__ tok(T_NS_C); + __dir__ tok(T_DIR); + attribute { + // expecting_xhp_class_statements is set in some actions in the grammar. + // This means the lexer and parser are interdependent. + if ((last_token() == '{' || last_token() == '}' || last_token() == ';') && + (yyextra->expecting_xhp_class_statements)) { + tok(T_XHP_ATTRIBUTE); + } else { + tok(T_STRING); + } + } + category { + if ((last_token() == '{' || last_token() == '}' || last_token() == ';') && + (yyextra->expecting_xhp_class_statements)) { + tok(T_XHP_CATEGORY); + } else { + tok(T_STRING); + } + } + children { + if ((last_token() == '{' || last_token() == '}' || last_token() == ';') && + (yyextra->expecting_xhp_class_statements)) { + tok(T_XHP_CHILDREN); + } else { + tok(T_STRING); + } + } +} + + /* Operators */ +{ + "+=" tok(T_PLUS_EQUAL); + "-=" tok(T_MINUS_EQUAL); + "*=" tok(T_MUL_EQUAL); + "/=" tok(T_DIV_EQUAL); + ".=" tok(T_CONCAT_EQUAL); + "%=" tok(T_MOD_EQUAL); + "&=" tok(T_AND_EQUAL); + "|=" tok(T_OR_EQUAL); + "^=" tok(T_XOR_EQUAL); + "<<=" tok(T_SL_EQUAL); + ">>=" tok(T_SR_EQUAL); + "||" tok(T_BOOLEAN_OR); + "&&" tok(T_BOOLEAN_AND); + "==" tok(T_IS_EQUAL); + "!="|"<>" tok(T_IS_NOT_EQUAL); + "===" tok(T_IS_IDENTICAL); + "!==" tok(T_IS_NOT_IDENTICAL); + "<=" tok(T_IS_SMALLER_OR_EQUAL); + ">=" tok(T_IS_GREATER_OR_EQUAL); + "<<" tok(T_SL); + ">>" tok(T_SR); + "++" tok(T_INC); + "--" tok(T_DEC); + "->" tok(T_OBJECT_OPERATOR); + "=>" tok(T_DOUBLE_ARROW); + "::" tok(T_PAAMAYIM_NEKUDOTAYIM); + "\\" tok(T_NS_SEPARATOR); + ":" { + // A colon can either mean the start (or component) of an XHP class, + // a ternary expression (as in 1?false:null), the colon of a 'case', + // or finally the start of a block in the old PHP syntax. The following + // disambiguate between the XHP case, which requires a special token, + // and the other cases. + switch (yyextra->last_token) { + // In a ternary expression, the colon must follow a full-fledged + // expression so seeing for instance a binary operator means + // it must be an XHP class. + case ',': case '=': case '|': case '^': case '&': case '<': case '>': + case '+': case '-': case '%': case '!': case '~': case '[': case '(': + case '{': case '.': + case T_LOGICAL_OR: case T_LOGICAL_XOR: case T_LOGICAL_AND: + case T_PLUS_EQUAL: case T_MINUS_EQUAL: case T_MUL_EQUAL: + case T_DIV_EQUAL: case T_CONCAT_EQUAL: case T_MOD_EQUAL: + case T_AND_EQUAL: case T_OR_EQUAL: case T_XOR_EQUAL: + case T_SL_EQUAL: case T_SR_EQUAL: case T_BOOLEAN_OR: + case T_BOOLEAN_AND: case T_IS_EQUAL: case T_IS_NOT_EQUAL: + case T_IS_IDENTICAL: case T_IS_NOT_IDENTICAL: case T_IS_SMALLER_OR_EQUAL: + case T_IS_GREATER_OR_EQUAL: + // An XHP class can also occur after certain keywords. Not sure + // we got them all covered though. + case T_ECHO: case T_RETURN: + case T_EXTENDS: case T_INSTANCEOF: case T_DOUBLE_ARROW: + case T_XHP_ATTRIBUTE: + tok(T_XHP_COLON); + break; + default: + tok(':'); + break; + } + } +} + + /* Casts */ +{ + "("{TABS_AND_SPACES}(int|integer){TABS_AND_SPACES}")" tok(T_INT_CAST); + "("{TABS_AND_SPACES}(real|double|float){TABS_AND_SPACES}")" tok(T_DOUBLE_CAST); + "("{TABS_AND_SPACES}string{TABS_AND_SPACES}")" tok(T_STRING_CAST); + "("{TABS_AND_SPACES}unicode{TABS_AND_SPACES}")" tok(T_UNICODE_CAST); + "("{TABS_AND_SPACES}binary{TABS_AND_SPACES}")" tok(T_BINARY_CAST); + "("{TABS_AND_SPACES}array{TABS_AND_SPACES}")" tok(T_ARRAY_CAST); + "("{TABS_AND_SPACES}object{TABS_AND_SPACES}")" tok(T_OBJECT_CAST); + "("{TABS_AND_SPACES}(bool|boolean){TABS_AND_SPACES}")" tok(T_BOOL_CAST); + "("{TABS_AND_SPACES}unset{TABS_AND_SPACES}")" tok(T_UNSET_CAST); +} + + /* Scalars (parsing these doesn't really matter since we just pass them through literally) */ +{ + {LNUM}|{HNUM} tok(T_LNUMBER); + {DNUM}|{EXPONENT_DNUM} tok(T_DNUMBER); + {LABEL} tok(T_STRING); + "$"{LABEL} tok(T_VARIABLE); + b?'(\\.|\\\n|[^\\']+)*'|b?\"(\\.|\\\n|[^\\\"]+)*\" { + yy_scan_newlines(yytext, yyg); + tok(T_CONSTANT_ENCAPSED_STRING); + } + `[^`]*` { + yy_scan_newlines(yytext, yyg); + tok(T_BACKTICKS_EXPR); + } +} + + /* (HERE|NOW)DOC's */ +b?"<<<"{TABS_AND_SPACES} { + push_state(PHP_HEREDOC_START); + yyextra->heredoc_yyleng = yyleng; + yymore(); +} +{ + "'"{LABEL}"'"|\"{LABEL}\" { + // Create a new string for the heredoc label. Since we're using yymore above + // yytext will actually start at the "<<<" and not the label. Use of + // heredoc_yyleng jumps past that. Then we add 1 to get past the " or '. The + // match is similar to calculate length. + yyextra->heredoc_label = string(yytext + yyextra->heredoc_yyleng + 1, yyleng - yyextra->heredoc_yyleng - 2); + set_state(PHP_HEREDOC_NSTART); + yyextra->heredoc_yyleng = yyleng; + yymore(); + } + {LABEL} { + yyextra->heredoc_label = string(yytext + yyextra->heredoc_yyleng); + set_state(PHP_HEREDOC_NSTART); + yyextra->heredoc_yyleng = yyleng; + yymore(); + } +} +{NEWLINE} { + ++yyextra->lineno; + yyextra->heredoc_data = yytext + yyleng; + set_state(PHP_HEREDOC_DATA); + yymore(); +} +{ + [^\r\n]*{NEWLINE} { + ++yyextra->lineno; + set_state(PHP_HEREDOC_NEWLINE); + yyextra->heredoc_yyleng = yyleng; + yymore(); + } +} +{ + {LABEL};?{NEWLINE} { + if (strncmp(yyextra->heredoc_label.c_str(), yytext + yyextra->heredoc_yyleng, yyextra->heredoc_label.size()) == 0) { + switch (yytext[yyextra->heredoc_yyleng + yyextra->heredoc_label.size()]) { + case ';': case '\n': case '\r': + yyless(yyleng - (yyleng - yyextra->heredoc_yyleng - yyextra->heredoc_label.size())); + pop_state(); + tok(T_HEREDOC); + } + } + ++yyextra->lineno; + yyextra->heredoc_yyleng = yyleng; + yymore(); + } + [^\r\n]+ { + set_state(PHP_HEREDOC_DATA); + yyextra->heredoc_yyleng = yyleng; + yymore(); + } + {NEWLINE} { + ++yyextra->lineno; + yyextra->heredoc_yyleng = yyleng; + yymore(); + } +} + + /* XHP */ +{ + {WHITESPACE}+ { + yy_scan_newlines(yytext, yyg); + ptok(T_WHITESPACE); + } +} +{ + ":" tok(T_XHP_COLON); + "-" tok(T_XHP_HYPHEN); + "::" { + pop_state(); + + yyextra->colon_hack = true; + tok(T_PAAMAYIM_NEKUDOTAYIM); + } + "--" { + pop_state(); + tok(T_DEC); + } + {WHITESPACE} { + yy_scan_newlines(yytext, yyg); + pop_state(); + tok(T_XHP_WHITESPACE); + } + {LABEL} tok(T_STRING); + . { + pop_state(); + tok(yytext[0]); + } +} + +{ + "="|"/"|">" tok(yytext[0]); + {WHITESPACE}+ { + yy_scan_newlines(yytext, yyg); + ptok(T_WHITESPACE); + } + {LABEL} tok(T_STRING); +} + +{ + [^&'\\"]+ tok(T_XHP_TEXT); + \" { + pop_state(); + tok('"'); + } +} + +{ + {WHITESPACE}+ { + /* ignore whitespace at the start */ + yy_scan_newlines(yytext, yyg); + ptok(T_WHITESPACE); + set_state(XHP_CHILD); + } + . { + yyless(0); + set_state(XHP_CHILD); + } +} + + /* Below we use tokt() (and not tok) which internally transits to + * the XHP_AFTER_ENT state. */ +{ + /* xml entities */ + (?-i:") tokt("\""); + (?-i:&) tokt("&"); + (?-i:') tokt("\\'"); + (?-i:<) tokt("<") + (?-i:>) tokt(">"); + + /* html entities */ + (?-i: ) tokt("\u00A0"); + (?-i:¡) tokt("\u00A1"); + (?-i:¢) tokt("\u00A2"); + (?-i:£) tokt("\u00A3"); + (?-i:¤) tokt("\u00A4"); + (?-i:¥) tokt("\u00A5"); + (?-i:¦) tokt("\u00A6"); + (?-i:§) tokt("\u00A7"); + (?-i:¨) tokt("\u00A8"); + (?-i:©) tokt("\u00A9"); + (?-i:ª) tokt("\u00AA"); + (?-i:«) tokt("\u00AB"); + (?-i:¬) tokt("\u00AC"); + (?-i:­) tokt("\u00AD"); + (?-i:®) tokt("\u00AE"); + (?-i:¯) tokt("\u00AF"); + (?-i:°) tokt("\u00B0"); + (?-i:±) tokt("\u00B1"); + (?-i:²) tokt("\u00B2"); + (?-i:³) tokt("\u00B3"); + (?-i:´) tokt("\u00B4"); + (?-i:µ) tokt("\u00B5"); + (?-i:¶) tokt("\u00B6"); + (?-i:·) tokt("\u00B7"); + (?-i:¸) tokt("\u00B8"); + (?-i:¹) tokt("\u00B9"); + (?-i:º) tokt("\u00BA"); + (?-i:») tokt("\u00BB"); + (?-i:¼) tokt("\u00BC"); + (?-i:½) tokt("\u00BD"); + (?-i:¾) tokt("\u00BE"); + (?-i:¿) tokt("\u00BF"); + (?-i:À) tokt("\u00C0"); + (?-i:Á) tokt("\u00C1"); + (?-i:Â) tokt("\u00C2"); + (?-i:Ã) tokt("\u00C3"); + (?-i:Ä) tokt("\u00C4"); + (?-i:Å) tokt("\u00C5"); + (?-i:Æ) tokt("\u00C6"); + (?-i:Ç) tokt("\u00C7"); + (?-i:È) tokt("\u00C8"); + (?-i:É) tokt("\u00C9"); + (?-i:Ê) tokt("\u00CA"); + (?-i:Ë) tokt("\u00CB"); + (?-i:Ì) tokt("\u00CC"); + (?-i:Í) tokt("\u00CD"); + (?-i:Î) tokt("\u00CE"); + (?-i:Ï) tokt("\u00CF"); + (?-i:Ð) tokt("\u00D0"); + (?-i:Ñ) tokt("\u00D1"); + (?-i:Ò) tokt("\u00D2"); + (?-i:Ó) tokt("\u00D3"); + (?-i:Ô) tokt("\u00D4"); + (?-i:Õ) tokt("\u00D5"); + (?-i:Ö) tokt("\u00D6"); + (?-i:×) tokt("\u00D7"); + (?-i:Ø) tokt("\u00D8"); + (?-i:Ù) tokt("\u00D9"); + (?-i:Ú) tokt("\u00DA"); + (?-i:Û) tokt("\u00DB"); + (?-i:Ü) tokt("\u00DC"); + (?-i:Ý) tokt("\u00DD"); + (?-i:Þ) tokt("\u00DE"); + (?-i:ß) tokt("\u00DF"); + (?-i:à) tokt("\u00E0"); + (?-i:á) tokt("\u00E1"); + (?-i:â) tokt("\u00E2"); + (?-i:ã) tokt("\u00E3"); + (?-i:ä) tokt("\u00E4"); + (?-i:å) tokt("\u00E5"); + (?-i:æ) tokt("\u00E6"); + (?-i:ç) tokt("\u00E7"); + (?-i:è) tokt("\u00E8"); + (?-i:é) tokt("\u00E9"); + (?-i:ê) tokt("\u00EA"); + (?-i:ë) tokt("\u00EB"); + (?-i:ì) tokt("\u00EC"); + (?-i:í) tokt("\u00ED"); + (?-i:î) tokt("\u00EE"); + (?-i:ï) tokt("\u00EF"); + (?-i:ð) tokt("\u00F0"); + (?-i:ñ) tokt("\u00F1"); + (?-i:ò) tokt("\u00F2"); + (?-i:ó) tokt("\u00F3"); + (?-i:ô) tokt("\u00F4"); + (?-i:õ) tokt("\u00F5"); + (?-i:ö) tokt("\u00F6"); + (?-i:÷) tokt("\u00F7"); + (?-i:ø) tokt("\u00F8"); + (?-i:ù) tokt("\u00F9"); + (?-i:ú) tokt("\u00FA"); + (?-i:û) tokt("\u00FB"); + (?-i:ü) tokt("\u00FC"); + (?-i:ý) tokt("\u00FD"); + (?-i:þ) tokt("\u00FE"); + (?-i:ÿ) tokt("\u00FF"); + (?-i:Œ) tokt("\u0152"); + (?-i:œ) tokt("\u0153"); + (?-i:Š) tokt("\u0160"); + (?-i:š) tokt("\u0161"); + (?-i:Ÿ) tokt("\u0178"); + (?-i:ƒ) tokt("\u0192"); + (?-i:ˆ) tokt("\u02C6"); + (?-i:˜) tokt("\u02DC"); + (?-i:Α) tokt("\u0391"); + (?-i:Β) tokt("\u0392"); + (?-i:Γ) tokt("\u0393"); + (?-i:Δ) tokt("\u0394"); + (?-i:Ε) tokt("\u0395"); + (?-i:Ζ) tokt("\u0396"); + (?-i:Η) tokt("\u0397"); + (?-i:Θ) tokt("\u0398"); + (?-i:Ι) tokt("\u0399"); + (?-i:Κ) tokt("\u039A"); + (?-i:Λ) tokt("\u039B"); + (?-i:Μ) tokt("\u039C"); + (?-i:Ν) tokt("\u039D"); + (?-i:Ξ) tokt("\u039E"); + (?-i:Ο) tokt("\u039F"); + (?-i:Π) tokt("\u03A0"); + (?-i:Ρ) tokt("\u03A1"); + (?-i:Σ) tokt("\u03A3"); + (?-i:Τ) tokt("\u03A4"); + (?-i:Υ) tokt("\u03A5"); + (?-i:Φ) tokt("\u03A6"); + (?-i:Χ) tokt("\u03A7"); + (?-i:Ψ) tokt("\u03A8"); + (?-i:Ω) tokt("\u03A9"); + (?-i:α) tokt("\u03B1"); + (?-i:β) tokt("\u03B2"); + (?-i:γ) tokt("\u03B3"); + (?-i:δ) tokt("\u03B4"); + (?-i:ε) tokt("\u03B5"); + (?-i:ζ) tokt("\u03B6"); + (?-i:η) tokt("\u03B7"); + (?-i:θ) tokt("\u03B8"); + (?-i:ι) tokt("\u03B9"); + (?-i:κ) tokt("\u03BA"); + (?-i:λ) tokt("\u03BB"); + (?-i:μ) tokt("\u03BC"); + (?-i:ν) tokt("\u03BD"); + (?-i:ξ) tokt("\u03BE"); + (?-i:ο) tokt("\u03BF"); + (?-i:π) tokt("\u03C0"); + (?-i:ρ) tokt("\u03C1"); + (?-i:ς) tokt("\u03C2"); + (?-i:σ) tokt("\u03C3"); + (?-i:τ) tokt("\u03C4"); + (?-i:υ) tokt("\u03C5"); + (?-i:φ) tokt("\u03C6"); + (?-i:χ) tokt("\u03C7"); + (?-i:ψ) tokt("\u03C8"); + (?-i:ω) tokt("\u03C9"); + (?-i:ϑ) tokt("\u03D1"); + (?-i:ϒ) tokt("\u03D2"); + (?-i:ϖ) tokt("\u03D6"); + (?-i: ) tokt("\u2002"); + (?-i: ) tokt("\u2003"); + (?-i: ) tokt("\u2009"); + (?-i:‌) tokt("\u200C"); + (?-i:‍) tokt("\u200D"); + (?-i:‎) tokt("\u200E"); + (?-i:‏) tokt("\u200F"); + (?-i:–) tokt("\u2013"); + (?-i:—) tokt("\u2014"); + (?-i:‘) tokt("\u2018"); + (?-i:’) tokt("\u2019"); + (?-i:‚) tokt("\u201A"); + (?-i:“) tokt("\u201C"); + (?-i:”) tokt("\u201D"); + (?-i:„) tokt("\u201E"); + (?-i:†) tokt("\u2020"); + (?-i:‡) tokt("\u2021"); + (?-i:•) tokt("\u2022"); + (?-i:…) tokt("\u2026"); + (?-i:‰) tokt("\u2030"); + (?-i:′) tokt("\u2032"); + (?-i:″) tokt("\u2033"); + (?-i:‹) tokt("\u2039"); + (?-i:›) tokt("\u203A"); + (?-i:‾) tokt("\u203E"); + (?-i:⁄) tokt("\u2044"); + (?-i:€) tokt("\u20AC"); + (?-i:ℑ) tokt("\u2111"); + (?-i:℘) tokt("\u2118"); + (?-i:ℜ) tokt("\u211C"); + (?-i:™) tokt("\u2122"); + (?-i:ℵ) tokt("\u2135"); + (?-i:←) tokt("\u2190"); + (?-i:↑) tokt("\u2191"); + (?-i:→) tokt("\u2192"); + (?-i:↓) tokt("\u2193"); + (?-i:↔) tokt("\u2194"); + (?-i:↵) tokt("\u21B5"); + (?-i:⇐) tokt("\u21D0"); + (?-i:⇑) tokt("\u21D1"); + (?-i:⇒) tokt("\u21D2"); + (?-i:⇓) tokt("\u21D3"); + (?-i:⇔) tokt("\u21D4"); + (?-i:∀) tokt("\u2200"); + (?-i:∂) tokt("\u2202"); + (?-i:∃) tokt("\u2203"); + (?-i:∅) tokt("\u2205"); + (?-i:∇) tokt("\u2207"); + (?-i:∈) tokt("\u2208"); + (?-i:∉) tokt("\u2209"); + (?-i:∋) tokt("\u220B"); + (?-i:∏) tokt("\u220F"); + (?-i:∑) tokt("\u2211"); + (?-i:−) tokt("\u2212"); + (?-i:∗) tokt("\u2217"); + (?-i:√) tokt("\u221A"); + (?-i:∝) tokt("\u221D"); + (?-i:∞) tokt("\u221E"); + (?-i:∠) tokt("\u2220"); + (?-i:∧) tokt("\u2227"); + (?-i:∨) tokt("\u2228"); + (?-i:∩) tokt("\u2229"); + (?-i:∪) tokt("\u222A"); + (?-i:∫) tokt("\u222B"); + (?-i:∴) tokt("\u2234"); + (?-i:∼) tokt("\u223C"); + (?-i:≅) tokt("\u2245"); + (?-i:≈) tokt("\u2248"); + (?-i:≠) tokt("\u2260"); + (?-i:≡) tokt("\u2261"); + (?-i:≤) tokt("\u2264"); + (?-i:≥) tokt("\u2265"); + (?-i:⊂) tokt("\u2282"); + (?-i:⊃) tokt("\u2283"); + (?-i:⊄) tokt("\u2284"); + (?-i:⊆) tokt("\u2286"); + (?-i:⊇) tokt("\u2287"); + (?-i:⊕) tokt("\u2295"); + (?-i:⊗) tokt("\u2297"); + (?-i:⊥) tokt("\u22A5"); + (?-i:⋅) tokt("\u22C5"); + (?-i:⌈) tokt("\u2308"); + (?-i:⌉) tokt("\u2309"); + (?-i:⌊) tokt("\u230A"); + (?-i:⌋) tokt("\u230B"); + (?-i:⟨) tokt("\u2329"); + (?-i:⟩) tokt("\u232A"); + (?-i:◊) tokt("\u25CA"); + (?-i:♠) tokt("\u2660"); + (?-i:♣) tokt("\u2663"); + (?-i:♥) tokt("\u2665"); + (?-i:♦) tokt("\u2666"); + + /* awesome entities */ + (?-i:&cloud;) tokt("\u2601"); + (?-i:&umbrella;) tokt("\u2602"); + (?-i:&snowman;) tokt("\u2603"); + (?-i:&snowflake;) tokt("\u2745"); + (?-i:&comet;) tokt("\u2604"); + (?-i:&thunderstorm;) tokt("\u2608"); + + /* pseudo entities */ + ' tokt("\\'"); + "\\" tokt("\\\\"); + + /* meta entities */ + (?-i:&#[0-9]+;) { + char buf[5]; + utf8ize(atoi(yytext + 2), buf); + tokt(buf); + } + (?-i:&#x)[A-F0-9]+; { + char buf[5]; + char *_; + utf8ize(strtol(yytext + 3, &_, 16), buf); + tokt(buf); + } + + /* not entities */ + & { + yymore(); + BEGIN(XHP_INVALID_ENTITY); + } +} + +{ + {BYTE}{1,10} { + for (char* ii = yytext; *ii; ++ii) { + if (*ii == ';') { + ii[1] = 0; + break; + } + } + if (!yyextra->terminated) { + yyextra->error = string("Invalid entity: (") + yytext + ")"; + yyextra->terminated = true; + } + } +} + +{ + [ \t\x0b\x0c\xa0\r\n]|\r\n { + if (*yytext == '\r' || *yytext == '\n') { + // Since we rewrite newlines into space we need to increment both line + // counters. The first_lineno increment is quite a hack, and makes it so + // that this ent is on the wrong line but it doesn't mess up the rest of + // the file. + ++yyextra->lineno; + ++yyextra->first_lineno; + } + pop_state(); + tok(T_XHP_TEXT); + } + . { + pop_state(); + yyless(0); + } +} + +{ + [^&'<>\\{ \t\x0b\x0c\xa0\r\n]+{WHITESPACE}? { + yy_scan_newlines(yytext, yyg); + tok(T_XHP_TEXT); + } + {WHITESPACE}* { + yy_scan_newlines(yytext, yyg); + tok(T_XHP_TEXT); + } + /* TODO: I removed {WHITESPACE}* from all of these but it needs to be restored + if the tree is unflattened. */ + "{" { + yy_scan_newlines(yytext, yyg); + tok('{'); + } + "<" { + yy_scan_newlines(yytext, yyg); + tok('<'); + } + "" { + yy_scan_newlines(yytext, yyg); + tok(T_XHP_LT_DIV_GT); + } +} + +{ + any tok(T_XHP_ANY); + pcdata tok(T_XHP_PCDATA); + empty tok(T_XHP_EMPTY); + {LABEL} tok(T_STRING); + ";" { + pop_state(); + tok(';'); + } + ":" { + tok(T_XHP_COLON); + } +} + + /* Other */ +<*>{BYTE} { + tok(yytext[0]); + // fix unused function warnings + yy_top_state(NULL); + yyunput(0, 0, NULL); +} + +%% + +#ifdef DEBUG +static const char* yy_state_name(int state) { + switch (state) { + case INITIAL: + return "INITIAL"; + case PHP: + return "PHP"; + case PHP_COMMENT: + return "PHP_COMMENT"; + case PHP_EOL_COMMENT: + return "PHP_EOL_COMMENT"; + case PHP_DOC_COMMENT: + return "PHP_DOC_COMMENT"; + case PHP_HEREDOC_START: + return "PHP_HEREDOC_START"; + case PHP_HEREDOC_NSTART: + return "PHP_HEREDOC_NSTART"; + case PHP_HEREDOC_NEWLINE: + return "PHP_HEREDOC_NEWLINE"; + case PHP_HEREDOC_DATA: + return "PHP_HEREDOC_DATA"; + case PHP_NO_RESERVED_WORDS: + return "PHP_NO_RESERVED_WORDS"; + case PHP_NO_RESERVED_WORDS_PERSIST: + return "PHP_NO_RESERVED_WORDS_PERSIST"; + case XHP_LABEL: + return "XHP_LABEL"; + case XHP_LABEL_WHITESPACE: + return "XHP_LABEL_WHITESPACE"; + case XHP_ATTRS: + return "XHP_ATTRS"; + case XHP_ATTR_VAL: + return "XHP_ATTR_VAL"; + case XHP_AFTER_ENT: + return "XHP_AFTER_ENT"; + case XHP_CHILD: + return "XHP_CHILD"; + case XHP_CHILD_START: + return "XHP_CHILD_START"; + case XHP_INVALID_ENTITY: + return "XHP_INVALID_ENTITY"; + case XHP_ATTR_TYPE_DECL: + return "XHP_ATTR_TYPE_DECL"; + case XHP_CHILDREN_DECL: + return "XHP_CHILDREN_DECL"; + default: + return "???"; + } +} + +static void yy_log_token(int tok) { + const char* tokname = yytokname(tok); + if (tokname) { + fprintf(stderr, "--> %s\n", tokname); + } else { + fprintf(stderr, "--> '%c'\n", tok); + } +} +#endif + +static int yy_token(int tok, yyguts_t* yyg) { + if (YY_START == PHP_NO_RESERVED_WORDS) { + pop_state(); + } + + switch (tok) { + case T_OPEN_TAG: + case T_OPEN_TAG_WITH_ECHO: + case T_OPEN_TAG_FAKE: + push_state(PHP); + break; + + case T_CLOSE_TAG: + pop_state(); + // We need to return a ';', not a T_CLOSE_TAG, because a construct like + // "" is valid and there are about a billion parser rules + // which terminate with ';' so making a new rule like + // "semicolon_or_close_tag" would be hard. The token in yylval has the + // correct type and value, we just don't generate a node. + return ';'; + + // In PHP it's ok to use keywords such as 'if' as field names + // or function names. + case T_OBJECT_OPERATOR: + case T_FUNCTION: + push_state(PHP_NO_RESERVED_WORDS); + break; + + case T_PAAMAYIM_NEKUDOTAYIM: + if (yyextra->colon_hack) { + yyextra->colon_hack = false; + } else { + push_state(PHP_NO_RESERVED_WORDS); + } + break; + + case '{': + // not used anymore + yyextra->curly_stack.push(tok); + break; + } +#ifdef DEBUG + yy_log_token(tok); +#endif + return yyextra->last_token = tok; +} + +static inline void yy_scan_newlines(const char* text, struct yyguts_t* yyg) { + for (; *text; ++text) { + if (*text == '\r') { + if (text[1] == '\n') { + ++text; + } + ++yyextra->lineno; + } else if (*text == '\n') { + ++yyextra->lineno; + } + } +} + +void xhp_new_push_state(int s, struct yyguts_t* yyg) { +#ifdef DEBUG + fprintf(stderr, "--> PUSH(%s -> %s)\n", yy_state_name(YY_START), yy_state_name(s)); +#endif + yy_push_state(s, yyg); +} + +void xhp_new_pop_state(struct yyguts_t* yyg) { +#ifdef DEBUG + int s = YY_START; +#endif + yy_pop_state(yyg); +#ifdef DEBUG + fprintf(stderr, "--> POP(%s -> %s)\n", yy_state_name(s), yy_state_name(YY_START)); +#endif +} + +void xhp_set_state(int s, struct yyguts_t* yyg) { +#ifdef DEBUG + fprintf(stderr, "--> SET(%s)\n", yy_state_name(s)); +#endif + BEGIN(s); +} diff --git a/support/xhpast/xhpast.cpp b/support/xhpast/xhpast.cpp new file mode 100755 index 00000000..a1619856 --- /dev/null +++ b/support/xhpast/xhpast.cpp @@ -0,0 +1,150 @@ +/* + * Copyright 2011 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ast.hpp" +#include +#include +#include +#include +#include +using namespace std; + +int xhpastparse(void*, xhpast::Node **); + +int xhpast_process(std::string &in); +void print_node(xhpast::Node *node); + +int main(int argc, char* argv[]) { + vector files; + +/* if (argc != 2) { + cerr << "usage: xhpast \n"; + return 1; + } + */ + + ifstream inputFile; + istream *inputStream; +// inputFile.open(argv[1]); +// inputStream = &inputFile; + inputStream = &cin; + + std::stringbuf sb; + *inputStream >> noskipws >> &sb; + std::string buffer = sb.str(); + inputFile.close(); + + return xhpast_process(buffer); +} + +int xhpast_process(std::string &in) { + + char *buffer; + in.reserve(in.size() + 1); + buffer = const_cast(in.c_str()); + buffer[in.size() + 1] = 0; // need double NULL for scan_buffer + + void* scanner; + yy_extra_type extra; + extra.idx_expr = true;//flags.idx_expr; + extra.include_debug = true;//flags.include_debug; + extra.insert_token = 0;//flags.eval ? T_OPEN_TAG_FAKE : 0; + extra.short_tags = true;//flags.short_tags; + extra.asp_tags = false;//flags.asp_tags; + + xhpast::Node *root = NULL; + + xhpastlex_init(&scanner); + xhpastset_extra(&extra, scanner); + xhpast_scan_buffer(buffer, in.size() + 2, scanner); +#ifdef DEBUG + xhpdebug = 1; +#endif + xhpastparse(scanner, &root); + xhpastlex_destroy(scanner); + + if (extra.terminated) { + fprintf( + stderr, + "XHPAST Parse Error: %s on line %d\n", + extra.error.c_str(), + (int)extra.lineno); + return 1; + } + + printf("{"); + printf("\"tree\":"); + if (root) { + // Extend the right token for the root node to the end of the concrete + // token stream. This ensure all tokens appear in the tree. If we don't + // do this and the file ends in tokens which don't go to the parser (like + // comments and whitespace) they won't be represented in the tree. + root->r_tok = (extra.token_list.size() - 1); + print_node(root); + } else { + printf("null"); + } + printf(","); + printf("\"stream\":"); + printf("["); + + if (!extra.token_list.empty()) { + for (xhpast::token_list_t::iterator ii = extra.token_list.begin();;) { + printf("[%d, %d]", (*ii)->type, (int)(*ii)->value.length()); + if (++ii != extra.token_list.end()) { + printf(","); + } else { + break; + } + } + } + printf("]"); + printf("}\n"); + + return 0; +} + +void print_node(xhpast::Node *node) { + int l = -1; + int r = -1; + if (node->l_tok != -1) { + l = node->l_tok; + } + + if (l == -1) { + printf("[%d]", node->type); + } else { + if (node->r_tok != -1) { + r = node->r_tok; + } + + printf("[%d, %d, %d", node->type, l, r); + if (!node->children.empty()) { + printf(", ["); + for (xhpast::node_list_t::iterator ii = node->children.begin();;) { + print_node(*ii); + if (++ii != node->children.end()) { + printf(","); + } else { + break; + } + } + printf("]"); + } + printf("]"); + } +} +