diff --git a/scripts/symbols/import_project_symbols.php b/scripts/symbols/import_project_symbols.php index 8aef684a40..4ff2a3f2cd 100755 --- a/scripts/symbols/import_project_symbols.php +++ b/scripts/symbols/import_project_symbols.php @@ -1,180 +1,172 @@ #!/usr/bin/env php <?php /* * Copyright 2012 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. */ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; $args = new PhutilArgumentParser($argv); $args->setSynopsis(<<<EOSYNOPSIS **import_project_symbols.php** [__options__] __project_name__ < symbols Import project symbols (symbols are read from stdin). EOSYNOPSIS ); $args->parseStandardArguments(); $args->parse( array( - array( - 'name' => 'ignore-duplicates', - 'help' => 'Ignore duplicate symbols, choosing one at random. By '. - 'default, this script throws if given duplicate '. - 'symbols.', - ), array( 'name' => 'more', 'wildcard' => true, ), )); -$ignore_duplicates = $args->getArg('ignore-duplicates'); $more = $args->getArg('more'); if (count($more) !== 1) { $args->printHelpAndExit(); } $project_name = head($more); $project = id(new PhabricatorRepositoryArcanistProject())->loadOneWhere( 'name = %s', $project_name); if (!$project) { // TODO: Provide a less silly way to do this explicitly, or just do it right // here. echo "Project '{$project_name}' is unknown. Upload a diff to implicitly ". "create it.\n"; exit(1); } echo "Parsing input from stdin...\n"; $input = file_get_contents('php://stdin'); $input = trim($input); $input = explode("\n", $input); -$map = array(); $symbols = array(); foreach ($input as $key => $line) { $line_no = $key + 1; $matches = null; - $ok = preg_match('/^([^ ]+) ([^ ]+) ([^ ]+) (\d+) (.*)$/', $line, $matches); + $ok = preg_match( + '/^((?P<context>[^ ]+)? )?(?P<name>[^ ]+) (?P<type>[^ ]+) '. + '(?P<lang>[^ ]+) (?P<line>\d+) (?P<path>.*)$/', + $line, + $matches); if (!$ok) { throw new Exception( - "Line #{$line_no} of input is invalid. Expected five space-delimited ". - "fields: symbol name, symbol type, symbol language, line number, path. ". + "Line #{$line_no} of input is invalid. Expected five or six ". + "space-delimited fields: maybe symbol context, symbol name, symbol ". + "type, symbol language, line number, path. ". "For example:\n\n". "idx function php 13 /path/to/some/file.php\n\n". "Actual line was:\n\n". "{$line}"); } - list($all, $name, $type, $lang, $line_number, $path) = $matches; - - if (isset($map[$name][$type][$lang])) { - if ($ignore_duplicates) { - echo "Ignoring duplicate definition of '{$name}' on line {$line_no}.\n"; - } else { - $previous = $map[$name][$type][$lang] + 1; - throw new Exception( - "Line #{$line_no} of input is invalid. It specifies a duplicate ". - "symbol (same name, language, and type) which has already been ". - "defined elsewhere. You must preprocess the symbol list to remove ". - "duplicates and choose exactly one master definition for each ". - "symbol, or specify --ignore-duplicates. This symbol was previously ". - "defined on line #{$previous}.\n\n". - "Line #{$line_no}:\n". - $line."\n\n". - "Line #{$previous}:\n". - $input[$previous - 1]); - } - } else { - $map[$name][$type][$lang] = $key; + if (empty($matches['context'])) { + $matches['context'] = ''; + } + $context = $matches['context']; + $name = $matches['name']; + $type = $matches['type']; + $lang = $matches['lang']; + $line_number = $matches['line']; + $path = $matches['path']; + + if (strlen($context) > 128) { + throw new Exception( + "Symbol context '{$context}' defined on line #{$line_no} is too long, ". + "maximum symbol context length is 128 characters."); } if (strlen($name) > 128) { throw new Exception( "Symbol name '{$name}' defined on line #{$line_no} is too long, maximum ". "symbol name length is 128 characters."); } if (strlen($type) > 12) { throw new Exception( "Symbol type '{$type}' defined on line #{$line_no} is too long, maximum ". "symbol type length is 12 characters."); } if (strlen($lang) > 32) { throw new Exception( "Symbol language '{$lang}' defined on line #{$line_no} is too long, ". "maximum symbol language length is 32 characters."); } if (!strlen($path) || $path[0] != 0) { throw new Exception( "Path '{$path}' defined on line #{$line_no} is invalid. Paths should be ". "begin with '/' and specify a path from the root of the project, like ". "'/src/utils/utils.php'."); } $symbols[] = array( + 'ctxt' => $context, 'name' => $name, 'type' => $type, 'lang' => $lang, 'line' => $line_number, 'path' => $path, ); } echo "Looking up path IDs...\n"; $path_map = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths( ipull($symbols, 'path')); $symbol = new PhabricatorRepositorySymbol(); $conn_w = $symbol->establishConnection('w'); echo "Preparing queries...\n"; $sql = array(); foreach ($symbols as $dict) { $sql[] = qsprintf( $conn_w, - '(%d, %s, %s, %s, %d, %d)', + '(%d, %s, %s, %s, %s, %d, %d)', $project->getID(), + $dict['ctxt'], $dict['name'], $dict['type'], $dict['lang'], $dict['line'], $path_map[$dict['path']]); } echo "Purging old symbols...\n"; queryfx( $conn_w, 'DELETE FROM %T WHERE arcanistProjectID = %d', $symbol->getTableName(), $project->getID()); echo "Loading ".number_format(count($sql))." symbols...\n"; foreach (array_chunk($sql, 128) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T - (arcanistProjectID, symbolName, symbolType, symbolLanguage, lineNumber, - pathID) VALUES %Q', + (arcanistProjectID, symbolContext, symbolName, symbolType, + symbolLanguage, lineNumber, pathID) VALUES %Q', $symbol->getTableName(), implode(', ', $chunk)); } echo "Done.\n"; diff --git a/src/applications/diffusion/controller/DiffusionSymbolController.php b/src/applications/diffusion/controller/DiffusionSymbolController.php index 42caa0aea6..74de67ead0 100644 --- a/src/applications/diffusion/controller/DiffusionSymbolController.php +++ b/src/applications/diffusion/controller/DiffusionSymbolController.php @@ -1,165 +1,172 @@ <?php /* * Copyright 2012 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 DiffusionSymbolController extends DiffusionController { private $name; public function willProcessRequest(array $data) { $this->name = $data['name']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $query = new DiffusionSymbolQuery(); $query->setName($this->name); + if ($request->getStr('context') !== null) { + $query->setContext($request->getStr('context')); + } + if ($request->getStr('type')) { $query->setType($request->getStr('type')); } if ($request->getStr('lang')) { $query->setLanguage($request->getStr('lang')); } if ($request->getStr('projects')) { $phids = $request->getStr('projects'); $phids = explode(',', $phids); $phids = array_filter($phids); if ($phids) { $projects = id(new PhabricatorRepositoryArcanistProject()) ->loadAllWhere( 'phid IN (%Ls)', $phids); $projects = mpull($projects, 'getID'); if ($projects) { $query->setProjectIDs($projects); } } } $query->needPaths(true); $query->needArcanistProjects(true); $query->needRepositories(true); $symbols = $query->execute(); // For PHP builtins, jump to php.net documentation. if ($request->getBool('jump') && count($symbols) == 0) { if ($request->getStr('lang') == 'php') { switch ($request->getStr('type')) { case 'function': $functions = get_defined_functions(); if (in_array($this->name, $functions['internal'])) { return id(new AphrontRedirectResponse()) ->setURI('http://www.php.net/function.'.$this->name); } break; case 'class': if (class_exists($this->name, false) || interface_exists($this->name, false)) { if (id(new ReflectionClass($this->name))->isInternal()) { return id(new AphrontRedirectResponse()) ->setURI('http://www.php.net/class.'.$this->name); } } break; } } } $rows = array(); foreach ($symbols as $symbol) { $project = $symbol->getArcanistProject(); if ($project) { $project_name = $project->getName(); } else { $project_name = '-'; } $file = phutil_escape_html($symbol->getPath()); $line = phutil_escape_html($symbol->getLineNumber()); $repo = $symbol->getRepository(); if ($repo) { $href = $symbol->getURI(); if ($request->getBool('jump') && count($symbols) == 1) { // If this is a clickthrough from Differential, just jump them // straight to the target if we got a single hit. return id(new AphrontRedirectResponse())->setURI($href); } $location = phutil_render_tag( 'a', array( 'href' => $href, ), phutil_escape_html($file.':'.$line)); } else if ($file) { $location = phutil_escape_html($file.':'.$line); } else { $location = '?'; } $rows[] = array( phutil_escape_html($symbol->getSymbolType()), + phutil_escape_html($symbol->getSymbolContext()), phutil_escape_html($symbol->getSymbolName()), phutil_escape_html($symbol->getSymbolLanguage()), phutil_escape_html($project_name), $location, ); } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Type', + 'Context', 'Name', 'Language', 'Project', 'File', )); $table->setColumnClasses( array( + '', '', 'pri', '', '', '', )); $table->setNoDataString( "No matching symbol could be found in any indexed project."); $panel = new AphrontPanelView(); $panel->setHeader('Similar Symbols'); $panel->appendChild($table); return $this->buildStandardPageResponse( array( $panel, ), array( 'title' => 'Find Symbol', )); } } diff --git a/src/applications/diffusion/query/DiffusionSymbolQuery.php b/src/applications/diffusion/query/DiffusionSymbolQuery.php index 25b6e49a1a..c8af76bdc8 100644 --- a/src/applications/diffusion/query/DiffusionSymbolQuery.php +++ b/src/applications/diffusion/query/DiffusionSymbolQuery.php @@ -1,278 +1,295 @@ <?php /* * Copyright 2012 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. */ /** * Query symbol information (class and function names and location), returning * a list of matching @{class:PhabricatorRepositorySymbol} objects and possibly * attached data. * * @task config Configuring the Query * @task exec Executing the Query * @task internal Internals * * @group diffusion */ final class DiffusionSymbolQuery extends PhabricatorOffsetPagedQuery { + private $context; private $namePrefix; private $name; private $projectIDs; private $language; private $type; private $needPaths; private $needArcanistProject; private $needRepositories; /* -( Configuring the Query )---------------------------------------------- */ + /** + * @task config + */ + public function setContext($context) { + $this->context = $context; + return $this; + } + + /** * @task config */ public function setName($name) { $this->name = $name; return $this; } /** * @task config */ public function setNamePrefix($name_prefix) { $this->namePrefix = $name_prefix; return $this; } /** * @task config */ public function setProjectIDs(array $project_ids) { $this->projectIDs = $project_ids; return $this; } /** * @task config */ public function setLanguage($language) { $this->language = $language; return $this; } /** * @task config */ public function setType($type) { $this->type = $type; return $this; } /** * @task config */ public function needPaths($need_paths) { $this->needPaths = $need_paths; return $this; } /** * @task config */ public function needArcanistProjects($need_arcanist_projects) { $this->needArcanistProjects = $need_arcanist_projects; return $this; } /** * @task config */ public function needRepositories($need_repositories) { $this->needRepositories = $need_repositories; return $this; } /* -( Executing the Query )------------------------------------------------ */ /** * @task exec */ public function execute() { if ($this->name && $this->namePrefix) { throw new Exception( "You can not set both a name and a name prefix!"); } else if (!$this->name && !$this->namePrefix) { throw new Exception( "You must set a name or a name prefix!"); } $symbol = new PhabricatorRepositorySymbol(); $conn_r = $symbol->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $symbol->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $symbols = $symbol->loadAllFromArray($data); if ($symbols) { if ($this->needPaths) { $this->loadPaths($symbols); } if ($this->needArcanistProjects || $this->needRepositories) { $this->loadArcanistProjects($symbols); } if ($this->needRepositories) { $this->loadRepositories($symbols); } } return $symbols; } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function buildOrderClause($conn_r) { return qsprintf( $conn_r, 'ORDER BY symbolName ASC'); } /** * @task internal */ private function buildWhereClause($conn_r) { $where = array(); + if (isset($this->context)) { + $where[] = qsprintf( + $conn_r, + 'symbolContext = %s', + $this->context); + } + if ($this->name) { $where[] = qsprintf( $conn_r, 'symbolName = %s', $this->name); } if ($this->namePrefix) { $where[] = qsprintf( $conn_r, 'symbolName LIKE %>', $this->namePrefix); } if ($this->projectIDs) { $where[] = qsprintf( $conn_r, 'arcanistProjectID IN (%Ld)', $this->projectIDs); } if ($this->language) { $where[] = qsprintf( $conn_r, 'symbolLanguage = %s', $this->language); } return $this->formatWhereClause($where); } /** * @task internal */ private function loadPaths(array $symbols) { assert_instances_of($symbols, 'PhabricatorRepositorySymbol'); $path_map = queryfx_all( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT * FROM %T WHERE id IN (%Ld)', PhabricatorRepository::TABLE_PATH, mpull($symbols, 'getPathID')); $path_map = ipull($path_map, 'path', 'id'); foreach ($symbols as $symbol) { $symbol->attachPath(idx($path_map, $symbol->getPathID())); } } /** * @task internal */ private function loadArcanistProjects(array $symbols) { assert_instances_of($symbols, 'PhabricatorRepositorySymbol'); $projects = id(new PhabricatorRepositoryArcanistProject())->loadAllWhere( 'id IN (%Ld)', mpull($symbols, 'getArcanistProjectID')); foreach ($symbols as $symbol) { $project = idx($projects, $symbol->getArcanistProjectID()); $symbol->attachArcanistProject($project); } } /** * @task internal */ private function loadRepositories(array $symbols) { assert_instances_of($symbols, 'PhabricatorRepositorySymbol'); $projects = mpull($symbols, 'getArcanistProject'); $projects = array_filter($projects); $repo_ids = mpull($projects, 'getRepositoryID'); $repo_ids = array_filter($repo_ids); if ($repo_ids) { $repos = id(new PhabricatorRepository())->loadAllWhere( 'id IN (%Ld)', $repo_ids); } else { $repos = array(); } foreach ($symbols as $symbol) { $proj = $symbol->getArcanistProject(); if ($proj) { $symbol->attachRepository(idx($repos, $proj->getRepositoryID())); } else { $symbol->attachRepository(null); } } } }