1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-09-19 00:18:50 +02:00

Initial commit.

This commit is contained in:
epriestley 2011-01-09 15:22:25 -08:00
commit 2e73916fa2
206 changed files with 19132 additions and 0 deletions

7
.arcconfig Normal file
View file

@ -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."
}

2
.divinerconfig Normal file
View file

@ -0,0 +1,2 @@
{}

9
.gitignore vendored Normal file
View file

@ -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

13
LICENSE Normal file
View file

@ -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.

11
README Normal file
View file

@ -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.

1
bin/arc Symbolic link
View file

@ -0,0 +1 @@
../scripts/arcanist.php

2
externals/README vendored Normal file
View file

@ -0,0 +1,2 @@
This directory contains third party open source software which is bundled with
Arcanist.

3
externals/pep8/README vendored Normal file
View file

@ -0,0 +1,3 @@
pep8.py was written by Johann Rocholl. The main page for the project is here:
https://github.com/jcrocholl/pep8

View file

@ -0,0 +1,32 @@
<?php
/*
* 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_path = ini_get('include_path');
ini_set('include_path', $include_path.':'.dirname(__FILE__).'/../../');
@require_once 'libphutil/src/__phutil_library_init__.php';
if (!@constant('__LIBPHUTIL__')) {
echo "ERROR: Unable to load libphutil. Update your PHP 'include_path' to ".
"include the parent directory of libphutil/.\n";
exit(1);
}
if (!ini_get('date.timezone')) {
date_default_timezone_set('America/Los_Angeles');
}
phutil_load_library(dirname(__FILE__).'/../src/');

181
scripts/arcanist.php Executable file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env php
<?php
/*
* 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.
*/
require_once dirname(__FILE__).'/__init_script__.php';
phutil_require_module('phutil', 'conduit/client');
phutil_require_module('phutil', 'console');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'autoload');
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'configuration');
phutil_require_module('arcanist', 'workingcopyidentity');
phutil_require_module('arcanist', 'repository/api/base');
$config_trace_mode = false;
$args = array_slice($argv, 1);
foreach ($args as $key => $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);
}

364
scripts/phutil_analyzer.php Executable file
View file

@ -0,0 +1,364 @@
#!/usr/bin/env php
<?php
/*
* 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.
*/
$builtin_classes = get_declared_classes();
$builtin_interfaces = get_declared_interfaces();
$builtin_functions = get_defined_functions();
$builtin_functions = $builtin_functions['internal'];
$builtin = array(
'class' => 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} <module>\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);
}

128
scripts/phutil_mapper.php Executable file
View file

@ -0,0 +1,128 @@
#!/usr/bin/env php
<?php
/*
* 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.
*/
require_once dirname(__FILE__).'/__init_script__.php';
if ($argc != 2) {
$self = basename($argv[0]);
echo "usage: {$self} <phutil_library_root>\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 = <<<EOPHP
<?php
/**
* This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
* {$at}generated
*/
phutil_register_library_map({$library_map});
EOPHP;
echo "Writing library map file...\n";
Filesystem::writeFile($root.'/__phutil_library_map__.php', $map_file);
echo "Done.\n";

View file

@ -0,0 +1,19 @@
<?php
/*
* 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.
*/
phutil_register_library('arcanist', __FILE__);

View file

@ -0,0 +1,114 @@
<?php
/**
* This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
* @generated
*/
phutil_register_library_map(array(
'class' =>
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(
),
));

View file

@ -0,0 +1,80 @@
<?php
/*
* 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.
*/
class ArcanistConfiguration {
public function buildWorkflow($command) {
if ($command == '--help') {
// Special-case "arc --help" to behave like "arc help" instead of telling
// you to type "arc help" without being helpful.
$command = 'help';
}
if ($command == 'base') {
return null;
}
if (!phutil_module_exists('arcanist', 'workflow/'.$command)) {
return null;
}
$workflow_class = 'Arcanist'.ucfirst($command).'Workflow';
$workflow_class = preg_replace_callback(
'/-([a-z])/',
array(
'ArcanistConfiguration',
'replaceClassnameHyphens',
),
$workflow_class);
phutil_autoload_class($workflow_class);
return newv($workflow_class, array());
}
public function buildAllWorkflows() {
$classes = phutil_find_class_descendants('ArcanistBaseWorkflow');
$workflows = array();
foreach ($classes as $class) {
$name = preg_replace('/^Arcanist(\w+)Workflow$/', '\1', $class);
$name = strtolower($name);
phutil_autoload_class($class);
$workflows[$name] = newv($class, array());
}
return $workflows;
}
public function willRunWorkflow($command, ArcanistBaseWorkflow $workflow) {
// This is a hook.
}
public function didRunWorkflow($command, ArcanistBaseWorkflow $workflow) {
// This is a hook.
}
public function getCustomArgumentsForCommand($command) {
return array();
}
public static function replaceClassnameHyphens($m) {
return strtoupper($m[1]);
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phutil', 'autoload');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistConfiguration.php');

View file

@ -0,0 +1,93 @@
<?php
/*
* 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.
*/
class ArcanistDifferentialCommitMessage {
private $rawCorpus;
private $revisionID;
private $fields;
private $gitSVNBaseRevision;
private $gitSVNBasePath;
private $gitSVNUUID;
public static function newFromRawCorpus($corpus) {
$obj = new ArcanistDifferentialCommitMessage();
$obj->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;
}
}

View file

@ -0,0 +1,21 @@
<?php
/*
* 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.
*/
class ArcanistDifferentialCommitMessageParserException extends Exception {
}

View file

@ -0,0 +1,11 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('ArcanistDifferentialCommitMessage.php');
phutil_require_source('ArcanistDifferentialCommitMessageParserException.php');

View file

@ -0,0 +1,55 @@
<?php
/*
* 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.
*/
class ArcanistDifferentialRevisionRef {
protected $id;
protected $name;
protected $statusName;
protected $sourcePath;
public function newFromDictionary(array $dictionary) {
$ref = new ArcanistDifferentialRevisionRef();
$ref->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;
}
}

View file

@ -0,0 +1,19 @@
<?php
/*
* 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.
*/
phutil_require_source('ArcanistDifferentialRevisionRef.php');

30
src/docs/overview.diviner Normal file
View file

@ -0,0 +1,30 @@
@title Arcanist Overview
This document provides an overview of Arcanist, a code workflow tool. Arcanist
(commonly, "arc") is the command-line frontend to Differential.
A detailed command reference is available by running ##arc help##.
= Overview =
Arcanist is the command-line interface to Differential, and supports some
related revision control operations. Arcanist allows you to do things like:
- send your code to Differential for review with ##arc diff##
- commit reviewed changes with ##arc commit## (svn) or ##arc amend## (git)
- check your code for syntax and style errors with ##arc lint##
- run unit tests that cover your changes with ##arc unit##
- export changes from Differential or the working copy with ##arc export##
- apply patches from Differential or patchfiles with ##arc patch##
- execute context-aware blame with ##arc cover##
- show Differential status with ##arc list##
In general, these workflows are agnostic to the underlying version control
system and will work properly in git or svn repositories.
= Configuring a New Project =
Create a .arcconfig file.
= SVN Basics =

View file

@ -0,0 +1,21 @@
<?php
/*
* 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.
*/
class ArcanistChooseInvalidRevisionException extends Exception {
}

View file

@ -0,0 +1,21 @@
<?php
/*
* 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.
*/
class ArcanistChooseNoRevisionsException extends Exception {
}

View file

@ -0,0 +1,20 @@
<?php
/*
* 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.
*/
phutil_require_source('ArcanistChooseInvalidRevisionException.php');
phutil_require_source('ArcanistChooseNoRevisionsException.php');

View file

@ -0,0 +1,21 @@
<?php
/*
* 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.
*/
class ArcanistUsageException extends Exception {
}

View file

@ -0,0 +1,10 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_source('ArcanistUsageException.php');

View file

@ -0,0 +1,20 @@
<?php
/*
* 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.
*/
class ArcanistNoEffectException extends ArcanistUsageException {
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage');
phutil_require_source('ArcanistNoEffectException.php');

View file

@ -0,0 +1,20 @@
<?php
/*
* 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.
*/
class ArcanistNoEngineException extends ArcanistUsageException {
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage');
phutil_require_source('ArcanistNoEngineException.php');

View file

@ -0,0 +1,23 @@
<?php
/*
* 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.
*/
class ArcanistUserAbortException extends ArcanistUsageException {
public function __construct() {
parent::__construct('User aborted the workflow.');
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage');
phutil_require_source('ArcanistUserAbortException.php');

View file

@ -0,0 +1,199 @@
<?php
/*
* 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.
*/
abstract class ArcanistLintEngine {
protected $workingCopy;
protected $fileData = array();
protected $charToLine = array();
protected $lineToFirstChar = array();
private $results = array();
private $minimumSeverity = null;
private $changedLines = array();
public function __construct() {
}
public function setWorkingCopy(ArcanistWorkingCopyIdentity $working_copy) {
$this->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);
}
}

View file

@ -0,0 +1,17 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage/noeffect');
phutil_require_module('arcanist', 'lint/result');
phutil_require_module('arcanist', 'lint/severity');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistLintEngine.php');

View file

@ -0,0 +1,96 @@
<?php
/*
* 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.
*/
class PhutilLintEngine extends ArcanistLintEngine {
public function buildLinters() {
$linters = array();
$paths = $this->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);
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/engine/base');
phutil_require_module('arcanist', 'lint/linter/filename');
phutil_require_module('arcanist', 'lint/linter/generated');
phutil_require_module('arcanist', 'lint/linter/phutilmodule');
phutil_require_module('arcanist', 'lint/linter/text');
phutil_require_module('arcanist', 'lint/linter/xhpast');
phutil_require_module('arcanist', 'lint/linter/apachelicense');
phutil_require_module('phutil', 'filesystem');
phutil_require_source('PhutilLintEngine.php');

View file

@ -0,0 +1,37 @@
<?php
/*
* 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.
*/
final class UnitTestableArcanistLintEngine extends ArcanistLintEngine {
protected $linters = array();
public function addLinter($linter) {
$this->linters[] = $linter;
return $this;
}
public function addFileData($path, $data) {
$this->fileData[$path] = $data;
return $this;
}
protected function buildLinters() {
return $this->linters;
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/engine/base');
phutil_require_source('UnitTestableArcanistLintEngine.php');

View file

@ -0,0 +1,99 @@
<?php
/*
* 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.
*/
class ArcanistApacheLicenseLinter extends ArcanistLinter {
const LINT_NO_LICENSE_HEADER = 1;
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'APACHELICENSE';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
self::LINT_NO_LICENSE_HEADER => '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 = <<<EOLICENSE
/*
* Copyright {$year} {$copyright_holder}
*
* 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.
*/
EOLICENSE;
foreach ($patterns as $pattern) {
$data = $this->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;
}
}
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'exception/usage');
phutil_require_module('arcanist', 'lint/linter/base');
phutil_require_source('ArcanistApacheLicenseLinter.php');

View file

@ -0,0 +1,181 @@
<?php
/*
* 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.
*/
abstract class ArcanistLinter {
protected $paths = array();
protected $data = array();
protected $engine;
protected $activePath;
protected $messages = array();
protected $stopAllLinters = false;
private $customSeverityMap = array();
public function setCustomSeverityMap(array $map) {
$this->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();
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/message');
phutil_require_module('arcanist', 'lint/severity');
phutil_require_source('ArcanistLinter.php');

View file

@ -0,0 +1,50 @@
<?php
/*
* 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.
*/
class ArcanistFilenameLinter extends ArcanistLinter {
const LINT_BAD_FILENAME = 1;
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'NAM';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
self::LINT_BAD_FILENAME => '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.');
}
}
}

View file

@ -0,0 +1,21 @@
<?php
/*
* 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.
*/
phutil_require_module('arcanist', 'lint/linter/base');
phutil_require_source('ArcanistFilenameLinter.php');

View file

@ -0,0 +1,34 @@
<?php
/**
* This linter just stops the lint process when a file is marked as generated
* code.
*/
class ArcanistGeneratedLinter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'GEN';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
);
}
public function lintPath($path) {
$data = $this->getData($path);
if (preg_match('/@generated/', $data)) {
$this->stopAllLinters();
}
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/linter/base');
phutil_require_source('ArcanistGeneratedLinter.php');

View file

@ -0,0 +1,78 @@
<?php
/*
* 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.
*/
class ArcanistPEP8Linter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'PEP8';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
);
}
public function getPEP8Options() {
return null;
}
public function lintPath($path) {
$pep8_bin = phutil_get_library_root('arcanist').'/externals/pep8/pep8.py';
$options = $this->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);
}
}
}

View file

@ -0,0 +1,17 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/linter/base');
phutil_require_module('arcanist', 'lint/message');
phutil_require_module('arcanist', 'lint/severity');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'moduleutils');
phutil_require_source('ArcanistPEP8Linter.php');

View file

@ -0,0 +1,478 @@
<?php
/*
* 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.
*/
class ArcanistPhutilModuleLinter extends ArcanistLinter {
const LINT_UNDECLARED_CLASS = 1;
const LINT_UNDECLARED_FUNCTION = 2;
const LINT_UNDECLARED_INTERFACE = 3;
const LINT_UNDECLARED_SOURCE = 4;
const LINT_UNUSED_MODULE = 5;
const LINT_UNUSED_SOURCE = 6;
const LINT_INIT_REBUILD = 7;
const LINT_UNKNOWN_CLASS = 8;
const LINT_UNKNOWN_FUNCTION = 9;
const LINT_ANALYZER_SIGNATURE = 100;
const LINT_ANALYZER_DYNAMIC = 101;
const LINT_ANALYZER_NO_INIT = 102;
const LINT_ANALYZER_MULTIPLE_CLASSES = 103;
const LINT_ANALYZER_CLASS_FILENAME = 104;
public function getLintNameMap() {
return array(
self::LINT_UNDECLARED_CLASS => '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[] = '<?php';
$at = '@';
$init[] = <<<EOHEADER
/**
* This file is automatically generated. Lint this module to rebuild it.
* {$at}generated
*/
EOHEADER;
$init[] = null;
$modules = $spec['requires']['module'];
foreach ($drop_modules as $drop) {
unset($modules[$drop]);
}
foreach ($need_classes as $need => $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;
}
}

View file

@ -0,0 +1,19 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/linter/base');
phutil_require_module('arcanist', 'lint/severity');
phutil_require_module('phutil', 'autoload');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'future');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'moduleutils');
phutil_require_source('ArcanistPhutilModuleLinter.php');

View file

@ -0,0 +1,184 @@
<?php
/*
* 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.
*/
class ArcanistTextLinter extends ArcanistLinter {
const LINT_DOS_NEWLINE = 1;
const LINT_TAB_LITERAL = 2;
const LINT_LINE_WRAP = 3;
const LINT_EOF_NEWLINE = 4;
const LINT_BAD_CHARSET = 5;
const LINT_TRAILING_WHITESPACE = 6;
private $maxLineLength = 80;
public function setMaxLineLength($new_length) {
$this->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,
'');
}
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/linter/base');
phutil_require_module('arcanist', 'lint/severity');
phutil_require_source('ArcanistTextLinter.php');

View file

@ -0,0 +1,909 @@
<?php
/*
* 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.
*/
class ArcanistXHPASTLinter extends ArcanistLinter {
private $trees = array();
const LINT_PHP_SYNTAX_ERROR = 1;
const LINT_UNABLE_TO_PARSE = 2;
const LINT_VARIABLE_VARIABLE = 3;
const LINT_EXTRACT_USE = 4;
const LINT_UNDECLARED_VARIABLE = 5;
const LINT_PHP_SHORT_TAG = 6;
const LINT_PHP_ECHO_TAG = 7;
const LINT_PHP_CLOSE_TAG = 8;
const LINT_NAMING_CONVENTIONS = 9;
const LINT_IMPLICIT_CONSTRUCTOR = 10;
const LINT_FORMATTING_CONVENTIONS = 11;
const LINT_DYNAMIC_DEFINE = 12;
const LINT_STATIC_THIS = 13;
const LINT_PREG_QUOTE_MISUSE = 14;
const LINT_PHP_OPEN_TAG = 15;
const LINT_TODO_COMMENT = 16;
const LINT_EXIT_EXPRESSION = 17;
const LINT_COMMENT_STYLE = 18;
public function getLintNameMap() {
return array(
self::LINT_PHP_SYNTAX_ERROR => '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 "<?"',
self::LINT_PHP_ECHO_TAG => 'Use of Echo Tag "<?="',
self::LINT_PHP_CLOSE_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()) == '<?') {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_SHORT_TAG,
'Use the full form of the PHP open tag, "<?php".',
"<?php\n");
}
break;
} else if ($token->getTypeName() == 'T_OPEN_TAG_WITH_ECHO') {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_ECHO_TAG,
'Avoid the PHP echo short form, "<?=".');
break;
} else {
if (!preg_match('/^#!/', $token->getValue())) {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_OPEN_TAG,
'PHP files should start with "<?php", which may be preceded by '.
'a "#!" line for scripts.');
}
}
}
foreach ($tokens as $token) {
if ($token->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);
}
}

View file

@ -0,0 +1,17 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/linter/base');
phutil_require_module('arcanist', 'lint/severity');
phutil_require_module('arcanist', 'staticanalysis/parsers/xhpast/api/tree');
phutil_require_module('arcanist', 'staticanalysis/parsers/xhpast/bin');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistXHPASTLinter.php');

View file

@ -0,0 +1,172 @@
<?php
/*
* 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.
*/
class ArcanistXHPASTLinterTestCase extends ArcanistPhutilTestCase {
public function testXHPASTLint() {
$root = realpath(dirname(__FILE__)).'/data/';
foreach (Filesystem::listDirectory($root, $hidden = false) as $file) {
$this->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.");
}
}

View file

@ -0,0 +1,19 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/engine/test');
phutil_require_module('arcanist', 'lint/linter/xhpast');
phutil_require_module('arcanist', 'lint/patcher');
phutil_require_module('arcanist', 'unit/engine/phutil/testcase');
phutil_require_module('arcanist', 'workingcopyidentity');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistXHPASTLinterTestCase.php');

View file

@ -0,0 +1,16 @@
<?php
$a []= 1;
$a[] = 1;
$a[]=1;
$a [] = 1;
~~~~~~~~~~
warning:2:3
warning:2:6
warning:4:5
warning:5:3
~~~~~~~~~~
<?php
$a[] = 1;
$a[] = 1;
$a[] = 1;
$a[] = 1;

View file

@ -0,0 +1,9 @@
<?php
define('pony', 'cute');
define($pony, 'cute');
define('pony', $cute);
define($pony, $cute);
<div>define($pony, $cute)</div>;
~~~~~~~~~~
error:3:8 dynamic define
error:5:8 dynamic define

View file

@ -0,0 +1,5 @@
<?php $x ?>
This shouldn't fatal the parser.
~~~~~~~~~~
error:1:10

View file

@ -0,0 +1,8 @@
<?php
exit(-1);
exit -1;
strtoupper(33 * exit - 6);
~~~~~~~~~~
error:3:1
warning:3:6
error:4:17

View file

@ -0,0 +1,81 @@
<?php
// This test is just making sure we have undone all of the stream elision that
// the default XHP grammar does, since it does certain transformations against
// whitespace and entities in the lexer itself. If our parse tree elides
// whitespace, the "+" operator trigger on the last line will patch at the wrong
// offset and the test will fail.
<div
id
=
{
$x
}
>
x
{
$x
}
<a />
&amp;
'
"
</div>
;
$x+$y;
~~~~~~~~~~
warning:39:3
~~~~~~~~~~
<?php
// This test is just making sure we have undone all of the stream elision that
// the default XHP grammar does, since it does certain transformations against
// whitespace and entities in the lexer itself. If our parse tree elides
// whitespace, the "+" operator trigger on the last line will patch at the wrong
// offset and the test will fail.
<div
id
=
{
$x
}
>
x
{
$x
}
<a />
&amp;
'
"
</div>
;
$x + $y;

View file

@ -0,0 +1,34 @@
<?php
# no
#no
// yes
# no
/* yes */ #no
/*
* yes
*/
/**
* yes
*/
//#yes
/*#yes*/
~~~~~~~~~~
error:2:1
error:3:1
error:5:1
error:6:11
~~~~~~~~~~
<?php
// no
//no
// yes
// no
/* yes */ //no
/*
* yes
*/
/**
* yes
*/
//#yes
/*#yes*/

View file

@ -0,0 +1,41 @@
<?php
class a {
const b = 1, c = d;
protected $E, $H;
public function F($G, $GG) { }
}
interface i { }
class :XhpStuff { }
function YY($ZZ) { }
class Quack {
const R = 1, S = 2;
protected $tX, $uY;
public function vV($w_w) { }
}
<div class="protected function">const $y;</div>;
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

View file

@ -0,0 +1,4 @@
<?php
abstract class A { }
final class F { }
~~~~~~~~~~

View file

@ -0,0 +1,3 @@
<?php
exit();
~~~~~~~~~~

View file

@ -0,0 +1,66 @@
<?php
if ( $x ) { }
f( );
q( );
g();
if ($x) { }
else if ( $y ) { }
<div>( a b c )</div>;
$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
~~~~~~~~~~
<?php
if ($x) { }
f();
q();
g();
if ($x) { }
else if ($y) { }
<div>( a b c )</div>;
$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
);

View file

@ -0,0 +1,14 @@
garbage garbage
<?
?>
~~~~~~~~~~
error:1:1
error:2:1
error:4:1
~~~~~~~~~~
garbage garbage
<?php
?>

View file

@ -0,0 +1,4 @@
<?=
~~~~~~~~~~
error:1:1

View file

@ -0,0 +1,4 @@
<?php
return;
~~~~~~~~~~

View file

@ -0,0 +1,5 @@
#!/usr/bin/local/php
<?php
return;
~~~~~~~~~~

View file

@ -0,0 +1,12 @@
<?php
function foo($bar) {
preg_quote($bar);
preg_quote($bar, '/');
preg_Quote('moo');
preg_quote('moo', '/');
}
~~~~~~~~~~
error:4:3 Wrong number of arguments to preg_quote()
error:6:3 Wrong number of arguments to preg_quote()

View file

@ -0,0 +1,16 @@
<?php
// This test is checking that the LintPatcher correctly applies adjacent patches
// with large character delta effects.
function f( ) {
g( );
}
~~~~~~~~~~
warning:4:12
warning:5:5
~~~~~~~~~~
<?php
// This test is checking that the LintPatcher correctly applies adjacent patches
// with large character delta effects.
function f() {
g();
}

View file

@ -0,0 +1,34 @@
<?php
if($x) {}
else{}
for(;;) {}
foreach($x as $y) {}
while($x) {}
do{} while($x);
switch($x) {}
if ($x) {}
else if ($y) {}
else if ($z) {}
<strong>if</strong>;
~~~~~~~~~~
warning:2:1
warning:3:1
warning:4:1
warning:5:1
warning:6:1
warning:7:1
warning:7:6
warning:8:1
~~~~~~~~~~
<?php
if ($x) {}
else {}
for (;;) {}
foreach ($x as $y) {}
while ($x) {}
do {} while ($x);
switch ($x) {}
if ($x) {}
else if ($y) {}
else if ($z) {}
<strong>if</strong>;

View file

@ -0,0 +1,59 @@
<?php
$a + $b;
$a+$b;
$a +$b;
$a+ $b;
$a = -$b;
$a=-$b;
$a-=$b;
$a -=$b;
$a-= $b;
$a===$b;
$a.$b;
function x($n=null) { }
function x($n = null) { }
<div id="id">1+1=2</div>;
$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
~~~~~~~~~~
<?php
$a + $b;
$a + $b;
$a + $b;
$a + $b;
$a = -$b;
$a = -$b;
$a -= $b;
$a -= $b;
$a -= $b;
$a === $b;
$a.$b;
function x($n=null) { }
function x($n = null) { }
<div id="id">1+1=2</div>;
$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) { }

View file

@ -0,0 +1,8 @@
<?php
class Platypus {
public function platypus() {
// This method must be renamed to __construct().
}
}
~~~~~~~~~~
error:3:19

View file

@ -0,0 +1,4 @@
<?php
char *buf = null;
~~~~~~~~~~
error:2:1

View file

@ -0,0 +1,175 @@
<?php
function () use ($c) {
$c++;
};
function f($a, $b) {
static $c, $d;
global $e, $f;
$g = $h = x();
list($i, list($j, $k)) = y();
foreach (q() as $l => $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

View file

@ -0,0 +1,86 @@
<?php
// This test is just verifying the parseability of files with a large number
// (>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.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.'';
~~~~~~~~~~
~~~~~~~~~~
<?php
// This test is just verifying the parseability of files with a large number
// (>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.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.'';

View file

@ -0,0 +1,13 @@
<?php
class A {
public function u() {
$this->f();
}
public static function v() {
$this->f();
}
}
~~~~~~~~~~
error:8:5 Use of $this in a static method.

View file

@ -0,0 +1,5 @@
<?php
$$foo;
$obj->$bar; // okay
~~~~~~~~~~
error:2:1

View file

@ -0,0 +1,169 @@
<?php
/*
* 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.
*/
class ArcanistLintMessage {
protected $path;
protected $line;
protected $char;
protected $code;
protected $severity;
protected $name;
protected $description;
protected $originalText;
protected $replacementText;
protected $appliedToDisk;
protected $dependentMessages = array();
public static function newFromDictionary(array $dict) {
$message = new ArcanistLintMessage();
$message->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;
}
}

View file

@ -0,0 +1,11 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/severity');
phutil_require_source('ArcanistLintMessage.php');

View file

@ -0,0 +1,148 @@
<?php
/*
* 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.
*/
final class ArcanistLintPatcher {
private $dirtyUntil = 0;
private $characterDelta = 0;
private $modifiedData = null;
private $lineOffsets = null;
private $lintResult = null;
private $applyMessages = array();
public static function newFromArcanistLintResult(ArcanistLintResult $result) {
$obj = new ArcanistLintPatcher();
$obj->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;
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistLintPatcher.php');

View file

@ -0,0 +1,189 @@
<?php
/*
* 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.
*/
class ArcanistLintRenderer {
private $summaryMode;
public function setSummaryMode($mode) {
$this->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(
" **<bg:{$color}> %s </bg>** (%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);
}
}

View file

@ -0,0 +1,14 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/severity');
phutil_require_module('phutil', 'console');
phutil_require_source('ArcanistLintRenderer.php');

View file

@ -0,0 +1,90 @@
<?php
/*
* 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.
*/
final class ArcanistLintResult {
protected $path;
protected $data;
protected $filePathOnDisk;
protected $messages = array();
private $needsSort;
public function setPath($path) {
$this->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;
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistLintResult.php');

View file

@ -0,0 +1,61 @@
<?php
/*
* 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.
*/
class ArcanistLintSeverity {
const SEVERITY_ADVICE = 'advice';
const SEVERITY_WARNING = 'warning';
const SEVERITY_ERROR = 'error';
const SEVERITY_DISABLED = 'disabled';
public static function getStringForSeverity($severity_code) {
static $map = array(
self::SEVERITY_ADVICE => '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);
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistLintSeverity.php');

View file

@ -0,0 +1,337 @@
<?php
/*
* 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.
*/
class ArcanistBundle {
private $changes;
public static function newFromChanges(array $changes) {
$obj = new ArcanistBundle();
$obj->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);
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'parser/diff');
phutil_require_module('arcanist', 'parser/diff/change');
phutil_require_module('arcanist', 'parser/diff/changetype');
phutil_require_module('arcanist', 'parser/diff/hunk');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'utils');
phutil_require_module('phutil', 'xsprintf/csprintf');
phutil_require_source('ArcanistBundle.php');

View file

@ -0,0 +1,815 @@
<?php
/*
* 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.
*/
class ArcanistDiffParser {
protected $api;
protected $text;
protected $line;
protected $isGit;
protected $detectBinaryFiles = false;
protected $changes = array();
protected function setRepositoryAPI(ArcanistRepositoryAPI $api) {
$this->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".
'(?<type>Index): (?<cur>.+)',
// This is an SVN property change, probably from "svn diff".
'(?<type>Property changes on): (?<cur>.+)',
// This is a git commit message, probably from "git show".
'(?<type>commit) (?<hash>[a-f0-9]+)',
// This is a git diff, probably from "git show" or "git diff".
'(?<type>diff --git) a/(?<old>.+) b/(?<cur>.+)',
// This is a unified diff, probably from "diff -u" or synthetic diffing.
'(?<type>---) (?<old>.+)\s+\d{4}-\d{2}-\d{2}.*',
'(?<binary>Binary) files '.
'(?<old>.+)\s+\d{4}-\d{2}-\d{2} and '.
'(?<new>.+)\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>new) file mode (?<newmode>\d+)',
'(?<deleted>deleted) file mode (?<oldmode>\d+)',
// These occur when someone uses `chmod` on a file.
'old mode (?<oldmode>\d+)',
'new mode (?<newmode>\d+)',
// These occur when you `mv` a file and git figures it out.
'similarity index ',
'rename from (?<old>.*)',
'(?<move>rename) to (?<cur>.*)',
'copy from (?<old>.*)',
'(?<copy>copy) to (?<cur>.*)'
);
$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]/)?(?<path>.*?)(?:\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);
}
}

View file

@ -0,0 +1,17 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'parser/diff/change');
phutil_require_module('arcanist', 'parser/diff/changetype');
phutil_require_module('arcanist', 'parser/diff/hunk');
phutil_require_module('arcanist', 'repository/api/base');
phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistDiffParser.php');

View file

@ -0,0 +1,458 @@
<?php
/*
* 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.
*/
class ArcanistDiffParserTestCase extends ArcanistPhutilTestCase {
public function testParser() {
$root = dirname(__FILE__).'/data/';
foreach (Filesystem::listDirectory($root, $hidden = false) as $file) {
$this->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 = <<<EOCORPUS
asdfasdf
+% quack
%
-%
%%
%%
%%%
EOCORPUS;
$corpus_1 = <<<EOCORPUS
%%%%%
%%%%%
{$there_is_a_literal_trailing_space_here}
-!
+! quack
EOCORPUS;
$this->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' => <<<EOTEXT
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
EOTEXT
));
break;
case 'svn-binary-add.svndiff':
$this->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(
<<<EOTEXT
Deprecating UIActionButton (Part 1)
Summary: Replaces calls to UIActionButton with <ui:button>. 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;
}
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'parser/diff');
phutil_require_module('arcanist', 'parser/diff/changetype');
phutil_require_module('arcanist', 'unit/engine/phutil/testcase');
phutil_require_module('phutil', 'filesystem');
phutil_require_source('ArcanistDiffParserTestCase.php');

View file

@ -0,0 +1 @@
Binary files a 9999-99-99 and b 9999-99-99 differ

View file

@ -0,0 +1,12 @@
--- a 2010-03-03 09:51:59.000000000 -0800
+++ b 2010-03-03 09:52:03.000000000 -0800
@@ -1 +1 @@
-a
\ No newline at end of file
+b
\ No newline at end of file
--- empty 2010-03-03 09:52:29.000000000 -0800
+++ full 2010-03-03 09:52:45.000000000 -0800
@@ -0,0 +1,2 @@
+duck
+quack

View file

@ -0,0 +1,7 @@
--- a 2010-03-03 09:51:59.000000000 -0800
+++ b 2010-03-03 09:52:03.000000000 -0800
@@ -1 +1 @@
-a
\ No newline at end of file
+b
\ No newline at end of file

Some files were not shown because too many files have changed in this diff Show more