mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-21 14:22:40 +01:00
Initial commit.
This commit is contained in:
commit
2e73916fa2
206 changed files with 19132 additions and 0 deletions
7
.arcconfig
Normal file
7
.arcconfig
Normal 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
2
.divinerconfig
Normal file
|
@ -0,0 +1,2 @@
|
|||
{}
|
||||
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal 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
13
LICENSE
Normal 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
11
README
Normal 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
1
bin/arc
Symbolic link
|
@ -0,0 +1 @@
|
|||
../scripts/arcanist.php
|
2
externals/README
vendored
Normal file
2
externals/README
vendored
Normal 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
3
externals/pep8/README
vendored
Normal 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
|
32
scripts/__init_script__.php
Normal file
32
scripts/__init_script__.php
Normal 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
181
scripts/arcanist.php
Executable 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
364
scripts/phutil_analyzer.php
Executable 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
128
scripts/phutil_mapper.php
Executable 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";
|
19
src/__phutil_library_init__.php
Normal file
19
src/__phutil_library_init__.php
Normal 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__);
|
114
src/__phutil_library_map__.php
Normal file
114
src/__phutil_library_map__.php
Normal 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(
|
||||
),
|
||||
));
|
80
src/configuration/ArcanistConfiguration.php
Normal file
80
src/configuration/ArcanistConfiguration.php
Normal 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]);
|
||||
}
|
||||
|
||||
}
|
13
src/configuration/__init__.php
Normal file
13
src/configuration/__init__.php
Normal 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');
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
11
src/differential/commitmessage/__init__.php
Normal file
11
src/differential/commitmessage/__init__.php
Normal 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');
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
19
src/differential/revision/__init__.php
Normal file
19
src/differential/revision/__init__.php
Normal 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
30
src/docs/overview.diviner
Normal 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 =
|
||||
|
21
src/exception/ArcanistChooseInvalidRevisionException.php
Normal file
21
src/exception/ArcanistChooseInvalidRevisionException.php
Normal 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 {
|
||||
|
||||
}
|
21
src/exception/ArcanistChooseNoRevisionsException.php
Normal file
21
src/exception/ArcanistChooseNoRevisionsException.php
Normal 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 {
|
||||
|
||||
}
|
20
src/exception/__init__.php
Normal file
20
src/exception/__init__.php
Normal 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');
|
21
src/exception/usage/ArcanistUsageException.php
Normal file
21
src/exception/usage/ArcanistUsageException.php
Normal 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 {
|
||||
|
||||
}
|
10
src/exception/usage/__init__.php
Normal file
10
src/exception/usage/__init__.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
phutil_require_source('ArcanistUsageException.php');
|
20
src/exception/usage/noeffect/ArcanistNoEffectException.php
Normal file
20
src/exception/usage/noeffect/ArcanistNoEffectException.php
Normal 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 {
|
||||
}
|
12
src/exception/usage/noeffect/__init__.php
Normal file
12
src/exception/usage/noeffect/__init__.php
Normal 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');
|
20
src/exception/usage/noengine/ArcanistNoEngineException.php
Normal file
20
src/exception/usage/noengine/ArcanistNoEngineException.php
Normal 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 {
|
||||
}
|
12
src/exception/usage/noengine/__init__.php
Normal file
12
src/exception/usage/noengine/__init__.php
Normal 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');
|
23
src/exception/usage/userabort/ArcanistUserAbortException.php
Normal file
23
src/exception/usage/userabort/ArcanistUserAbortException.php
Normal 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.');
|
||||
}
|
||||
}
|
12
src/exception/usage/userabort/__init__.php
Normal file
12
src/exception/usage/userabort/__init__.php
Normal 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');
|
199
src/lint/engine/base/ArcanistLintEngine.php
Normal file
199
src/lint/engine/base/ArcanistLintEngine.php
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
17
src/lint/engine/base/__init__.php
Normal file
17
src/lint/engine/base/__init__.php
Normal 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');
|
96
src/lint/engine/phutil/PhutilLintEngine.php
Normal file
96
src/lint/engine/phutil/PhutilLintEngine.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
20
src/lint/engine/phutil/__init__.php
Normal file
20
src/lint/engine/phutil/__init__.php
Normal 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');
|
37
src/lint/engine/test/UnitTestableArcanistLintEngine.php
Normal file
37
src/lint/engine/test/UnitTestableArcanistLintEngine.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
12
src/lint/engine/test/__init__.php
Normal file
12
src/lint/engine/test/__init__.php
Normal 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');
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
13
src/lint/linter/apachelicense/__init__.php
Normal file
13
src/lint/linter/apachelicense/__init__.php
Normal 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');
|
181
src/lint/linter/base/ArcanistLinter.php
Normal file
181
src/lint/linter/base/ArcanistLinter.php
Normal 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();
|
||||
|
||||
}
|
12
src/lint/linter/base/__init__.php
Normal file
12
src/lint/linter/base/__init__.php
Normal 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');
|
50
src/lint/linter/filename/ArcanistFilenameLinter.php
Normal file
50
src/lint/linter/filename/ArcanistFilenameLinter.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
21
src/lint/linter/filename/__init__.php
Normal file
21
src/lint/linter/filename/__init__.php
Normal 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');
|
34
src/lint/linter/generated/ArcanistGeneratedLinter.php
Normal file
34
src/lint/linter/generated/ArcanistGeneratedLinter.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
12
src/lint/linter/generated/__init__.php
Normal file
12
src/lint/linter/generated/__init__.php
Normal 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');
|
78
src/lint/linter/pep8/ArcanistPEP8Linter.php
Normal file
78
src/lint/linter/pep8/ArcanistPEP8Linter.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
17
src/lint/linter/pep8/__init__.php
Normal file
17
src/lint/linter/pep8/__init__.php
Normal 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');
|
478
src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php
Normal file
478
src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
19
src/lint/linter/phutilmodule/__init__.php
Normal file
19
src/lint/linter/phutilmodule/__init__.php
Normal 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');
|
184
src/lint/linter/text/ArcanistTextLinter.php
Normal file
184
src/lint/linter/text/ArcanistTextLinter.php
Normal 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,
|
||||
'');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
13
src/lint/linter/text/__init__.php
Normal file
13
src/lint/linter/text/__init__.php
Normal 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');
|
909
src/lint/linter/xhpast/ArcanistXHPASTLinter.php
Normal file
909
src/lint/linter/xhpast/ArcanistXHPASTLinter.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
17
src/lint/linter/xhpast/__init__.php
Normal file
17
src/lint/linter/xhpast/__init__.php
Normal 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');
|
|
@ -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.");
|
||||
}
|
||||
}
|
19
src/lint/linter/xhpast/__tests__/__init__.php
Normal file
19
src/lint/linter/xhpast/__tests__/__init__.php
Normal 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');
|
16
src/lint/linter/xhpast/__tests__/data/array-index.lint-test
Normal file
16
src/lint/linter/xhpast/__tests__/data/array-index.lint-test
Normal 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;
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
<?php $x ?>
|
||||
|
||||
This shouldn't fatal the parser.
|
||||
~~~~~~~~~~
|
||||
error:1:10
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
exit(-1);
|
||||
exit -1;
|
||||
strtoupper(33 * exit - 6);
|
||||
~~~~~~~~~~
|
||||
error:3:1
|
||||
warning:3:6
|
||||
error:4:17
|
|
@ -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 />
|
||||
|
||||
&
|
||||
|
||||
'
|
||||
|
||||
"
|
||||
|
||||
</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 />
|
||||
|
||||
&
|
||||
|
||||
'
|
||||
|
||||
"
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
;
|
||||
|
||||
$x + $y;
|
|
@ -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*/
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
abstract class A { }
|
||||
final class F { }
|
||||
~~~~~~~~~~
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
exit();
|
||||
~~~~~~~~~~
|
|
@ -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
|
||||
);
|
14
src/lint/linter/xhpast/__tests__/data/php-tags-bad.lint-test
Normal file
14
src/lint/linter/xhpast/__tests__/data/php-tags-bad.lint-test
Normal file
|
@ -0,0 +1,14 @@
|
|||
garbage garbage
|
||||
<?
|
||||
|
||||
?>
|
||||
~~~~~~~~~~
|
||||
error:1:1
|
||||
error:2:1
|
||||
error:4:1
|
||||
~~~~~~~~~~
|
||||
garbage garbage
|
||||
<?php
|
||||
|
||||
|
||||
?>
|
|
@ -0,0 +1,4 @@
|
|||
<?=
|
||||
|
||||
~~~~~~~~~~
|
||||
error:1:1
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
return;
|
||||
~~~~~~~~~~
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/local/php
|
||||
<?php
|
||||
|
||||
return;
|
||||
~~~~~~~~~~
|
12
src/lint/linter/xhpast/__tests__/data/preg-quote.lint-test
Normal file
12
src/lint/linter/xhpast/__tests__/data/preg-quote.lint-test
Normal 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()
|
|
@ -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();
|
||||
}
|
|
@ -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>;
|
|
@ -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) { }
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
class Platypus {
|
||||
public function platypus() {
|
||||
// This method must be renamed to __construct().
|
||||
}
|
||||
}
|
||||
~~~~~~~~~~
|
||||
error:3:19
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
char *buf = null;
|
||||
~~~~~~~~~~
|
||||
error:2:1
|
|
@ -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
|
|
@ -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.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.
|
||||
''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.''.'';
|
|
@ -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.
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
$$foo;
|
||||
$obj->$bar; // okay
|
||||
~~~~~~~~~~
|
||||
error:2:1
|
169
src/lint/message/ArcanistLintMessage.php
Normal file
169
src/lint/message/ArcanistLintMessage.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
11
src/lint/message/__init__.php
Normal file
11
src/lint/message/__init__.php
Normal 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');
|
148
src/lint/patcher/ArcanistLintPatcher.php
Normal file
148
src/lint/patcher/ArcanistLintPatcher.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
14
src/lint/patcher/__init__.php
Normal file
14
src/lint/patcher/__init__.php
Normal 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');
|
189
src/lint/renderer/ArcanistLintRenderer.php
Normal file
189
src/lint/renderer/ArcanistLintRenderer.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
14
src/lint/renderer/__init__.php
Normal file
14
src/lint/renderer/__init__.php
Normal 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');
|
90
src/lint/result/ArcanistLintResult.php
Normal file
90
src/lint/result/ArcanistLintResult.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
12
src/lint/result/__init__.php
Normal file
12
src/lint/result/__init__.php
Normal 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');
|
61
src/lint/severity/ArcanistLintSeverity.php
Normal file
61
src/lint/severity/ArcanistLintSeverity.php
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
12
src/lint/severity/__init__.php
Normal file
12
src/lint/severity/__init__.php
Normal 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');
|
337
src/parser/bundle/ArcanistBundle.php
Normal file
337
src/parser/bundle/ArcanistBundle.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
20
src/parser/bundle/__init__.php
Normal file
20
src/parser/bundle/__init__.php
Normal 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');
|
815
src/parser/diff/ArcanistDiffParser.php
Normal file
815
src/parser/diff/ArcanistDiffParser.php
Normal 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);
|
||||
}
|
||||
}
|
17
src/parser/diff/__init__.php
Normal file
17
src/parser/diff/__init__.php
Normal 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');
|
458
src/parser/diff/__tests__/ArcanistDiffParserTestCase.php
Normal file
458
src/parser/diff/__tests__/ArcanistDiffParserTestCase.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
16
src/parser/diff/__tests__/__init__.php
Normal file
16
src/parser/diff/__tests__/__init__.php
Normal 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');
|
1
src/parser/diff/__tests__/data/basic-binary.udiff
Normal file
1
src/parser/diff/__tests__/data/basic-binary.udiff
Normal file
|
@ -0,0 +1 @@
|
|||
Binary files a 9999-99-99 and b 9999-99-99 differ
|
12
src/parser/diff/__tests__/data/basic-missing-both-newlines-plus.udiff
Executable file
12
src/parser/diff/__tests__/data/basic-missing-both-newlines-plus.udiff
Executable 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
|
|
@ -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
Loading…
Reference in a new issue