Imported Upstream version 6.10.0.49

Former-commit-id: 1d6753294b2993e1fbf92de9366bb9544db4189b
This commit is contained in:
Xamarin Public Jenkins (auto-signing)
2020-01-16 16:38:04 +00:00
parent d94e79959b
commit 468663ddbb
48518 changed files with 2789335 additions and 61176 deletions

View File

@@ -0,0 +1,27 @@
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..)
add_clang_executable(clang-include-fixer
ClangIncludeFixer.cpp
)
target_link_libraries(clang-include-fixer
PRIVATE
clangBasic
clangFormat
clangFrontend
clangIncludeFixer
clangRewrite
clangTooling
clangToolingCore
findAllSymbols
)
install(TARGETS clang-include-fixer
RUNTIME DESTINATION bin)
install(PROGRAMS clang-include-fixer.el
DESTINATION share/clang
COMPONENT clang-include-fixer)
install(PROGRAMS clang-include-fixer.py
DESTINATION share/clang
COMPONENT clang-include-fixer)

View File

@@ -0,0 +1,471 @@
//===-- ClangIncludeFixer.cpp - Standalone include fixer ------------------===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
#include "FuzzySymbolIndex.h"
#include "InMemorySymbolIndex.h"
#include "IncludeFixer.h"
#include "IncludeFixerContext.h"
#include "SymbolIndexManager.h"
#include "YamlSymbolIndex.h"
#include "clang/Format/Format.h"
#include "clang/Frontend/TextDiagnosticPrinter.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Core/Replacement.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/YAMLTraits.h"
using namespace clang;
using namespace llvm;
using clang::include_fixer::IncludeFixerContext;
LLVM_YAML_IS_DOCUMENT_LIST_VECTOR(IncludeFixerContext)
LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(IncludeFixerContext::HeaderInfo)
LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(IncludeFixerContext::QuerySymbolInfo)
namespace llvm {
namespace yaml {
template <> struct MappingTraits<tooling::Range> {
struct NormalizedRange {
NormalizedRange(const IO &) : Offset(0), Length(0) {}
NormalizedRange(const IO &, const tooling::Range &R)
: Offset(R.getOffset()), Length(R.getLength()) {}
tooling::Range denormalize(const IO &) {
return tooling::Range(Offset, Length);
}
unsigned Offset;
unsigned Length;
};
static void mapping(IO &IO, tooling::Range &Info) {
MappingNormalization<NormalizedRange, tooling::Range> Keys(IO, Info);
IO.mapRequired("Offset", Keys->Offset);
IO.mapRequired("Length", Keys->Length);
}
};
template <> struct MappingTraits<IncludeFixerContext::HeaderInfo> {
static void mapping(IO &io, IncludeFixerContext::HeaderInfo &Info) {
io.mapRequired("Header", Info.Header);
io.mapRequired("QualifiedName", Info.QualifiedName);
}
};
template <> struct MappingTraits<IncludeFixerContext::QuerySymbolInfo> {
static void mapping(IO &io, IncludeFixerContext::QuerySymbolInfo &Info) {
io.mapRequired("RawIdentifier", Info.RawIdentifier);
io.mapRequired("Range", Info.Range);
}
};
template <> struct MappingTraits<IncludeFixerContext> {
static void mapping(IO &IO, IncludeFixerContext &Context) {
IO.mapRequired("QuerySymbolInfos", Context.QuerySymbolInfos);
IO.mapRequired("HeaderInfos", Context.HeaderInfos);
IO.mapRequired("FilePath", Context.FilePath);
}
};
} // namespace yaml
} // namespace llvm
namespace {
cl::OptionCategory IncludeFixerCategory("Tool options");
enum DatabaseFormatTy {
fixed, ///< Hard-coded mapping.
yaml, ///< Yaml database created by find-all-symbols.
fuzzyYaml, ///< Yaml database with fuzzy-matched identifiers.
};
cl::opt<DatabaseFormatTy> DatabaseFormat(
"db", cl::desc("Specify input format"),
cl::values(clEnumVal(fixed, "Hard-coded mapping"),
clEnumVal(yaml, "Yaml database created by find-all-symbols"),
clEnumVal(fuzzyYaml, "Yaml database, with fuzzy-matched names")),
cl::init(yaml), cl::cat(IncludeFixerCategory));
cl::opt<std::string> Input("input",
cl::desc("String to initialize the database"),
cl::cat(IncludeFixerCategory));
cl::opt<std::string>
QuerySymbol("query-symbol",
cl::desc("Query a given symbol (e.g. \"a::b::foo\") in\n"
"database directly without parsing the file."),
cl::cat(IncludeFixerCategory));
cl::opt<bool>
MinimizeIncludePaths("minimize-paths",
cl::desc("Whether to minimize added include paths"),
cl::init(true), cl::cat(IncludeFixerCategory));
cl::opt<bool> Quiet("q", cl::desc("Reduce terminal output"), cl::init(false),
cl::cat(IncludeFixerCategory));
cl::opt<bool>
STDINMode("stdin",
cl::desc("Override source file's content (in the overlaying\n"
"virtual file system) with input from <stdin> and run\n"
"the tool on the new content with the compilation\n"
"options of the source file. This mode is currently\n"
"used for editor integration."),
cl::init(false), cl::cat(IncludeFixerCategory));
cl::opt<bool> OutputHeaders(
"output-headers",
cl::desc("Print the symbol being queried and all its relevant headers in\n"
"JSON format to stdout:\n"
" {\n"
" \"FilePath\": \"/path/to/foo.cc\",\n"
" \"QuerySymbolInfos\": [\n"
" {\"RawIdentifier\": \"foo\",\n"
" \"Range\": {\"Offset\": 0, \"Length\": 3}}\n"
" ],\n"
" \"HeaderInfos\": [ {\"Header\": \"\\\"foo_a.h\\\"\",\n"
" \"QualifiedName\": \"a::foo\"} ]\n"
" }"),
cl::init(false), cl::cat(IncludeFixerCategory));
cl::opt<std::string> InsertHeader(
"insert-header",
cl::desc("Insert a specific header. This should run with STDIN mode.\n"
"The result is written to stdout. It is currently used for\n"
"editor integration. Support YAML/JSON format:\n"
" -insert-header=\"{\n"
" FilePath: \"/path/to/foo.cc\",\n"
" QuerySymbolInfos: [\n"
" {RawIdentifier: foo,\n"
" Range: {Offset: 0, Length: 3}}\n"
" ],\n"
" HeaderInfos: [ {Headers: \"\\\"foo_a.h\\\"\",\n"
" QualifiedName: \"a::foo\"} ]}\""),
cl::init(""), cl::cat(IncludeFixerCategory));
cl::opt<std::string>
Style("style",
cl::desc("Fallback style for reformatting after inserting new\n"
"headers if there is no clang-format config file found."),
cl::init("llvm"), cl::cat(IncludeFixerCategory));
std::unique_ptr<include_fixer::SymbolIndexManager>
createSymbolIndexManager(StringRef FilePath) {
using find_all_symbols::SymbolInfo;
auto SymbolIndexMgr = llvm::make_unique<include_fixer::SymbolIndexManager>();
switch (DatabaseFormat) {
case fixed: {
// Parse input and fill the database with it.
// <symbol>=<header><, header...>
// Multiple symbols can be given, separated by semicolons.
std::map<std::string, std::vector<std::string>> SymbolsMap;
SmallVector<StringRef, 4> SemicolonSplits;
StringRef(Input).split(SemicolonSplits, ";");
std::vector<find_all_symbols::SymbolAndSignals> Symbols;
for (StringRef Pair : SemicolonSplits) {
auto Split = Pair.split('=');
std::vector<std::string> Headers;
SmallVector<StringRef, 4> CommaSplits;
Split.second.split(CommaSplits, ",");
for (size_t I = 0, E = CommaSplits.size(); I != E; ++I)
Symbols.push_back(
{SymbolInfo(Split.first.trim(), SymbolInfo::SymbolKind::Unknown,
CommaSplits[I].trim(), {}),
// Use fake "seen" signal for tests, so first header wins.
SymbolInfo::Signals(/*Seen=*/static_cast<unsigned>(E - I),
/*Used=*/0)});
}
SymbolIndexMgr->addSymbolIndex([=]() {
return llvm::make_unique<include_fixer::InMemorySymbolIndex>(Symbols);
});
break;
}
case yaml: {
auto CreateYamlIdx = [=]() -> std::unique_ptr<include_fixer::SymbolIndex> {
llvm::ErrorOr<std::unique_ptr<include_fixer::YamlSymbolIndex>> DB(
nullptr);
if (!Input.empty()) {
DB = include_fixer::YamlSymbolIndex::createFromFile(Input);
} else {
// If we don't have any input file, look in the directory of the
// first
// file and its parents.
SmallString<128> AbsolutePath(tooling::getAbsolutePath(FilePath));
StringRef Directory = llvm::sys::path::parent_path(AbsolutePath);
DB = include_fixer::YamlSymbolIndex::createFromDirectory(
Directory, "find_all_symbols_db.yaml");
}
if (!DB) {
llvm::errs() << "Couldn't find YAML db: " << DB.getError().message()
<< '\n';
return nullptr;
}
return std::move(*DB);
};
SymbolIndexMgr->addSymbolIndex(std::move(CreateYamlIdx));
break;
}
case fuzzyYaml: {
// This mode is not very useful, because we don't correct the identifier.
// It's main purpose is to expose FuzzySymbolIndex to tests.
SymbolIndexMgr->addSymbolIndex(
[]() -> std::unique_ptr<include_fixer::SymbolIndex> {
auto DB = include_fixer::FuzzySymbolIndex::createFromYAML(Input);
if (!DB) {
llvm::errs() << "Couldn't load fuzzy YAML db: "
<< llvm::toString(DB.takeError()) << '\n';
return nullptr;
}
return std::move(*DB);
});
break;
}
}
return SymbolIndexMgr;
}
void writeToJson(llvm::raw_ostream &OS, const IncludeFixerContext& Context) {
OS << "{\n"
<< " \"FilePath\": \""
<< llvm::yaml::escape(Context.getFilePath()) << "\",\n"
<< " \"QuerySymbolInfos\": [\n";
for (const auto &Info : Context.getQuerySymbolInfos()) {
OS << " {\"RawIdentifier\": \"" << Info.RawIdentifier << "\",\n";
OS << " \"Range\":{";
OS << "\"Offset\":" << Info.Range.getOffset() << ",";
OS << "\"Length\":" << Info.Range.getLength() << "}}";
if (&Info != &Context.getQuerySymbolInfos().back())
OS << ",\n";
}
OS << "\n ],\n";
OS << " \"HeaderInfos\": [\n";
const auto &HeaderInfos = Context.getHeaderInfos();
for (const auto &Info : HeaderInfos) {
OS << " {\"Header\": \"" << llvm::yaml::escape(Info.Header) << "\",\n"
<< " \"QualifiedName\": \"" << Info.QualifiedName << "\"}";
if (&Info != &HeaderInfos.back())
OS << ",\n";
}
OS << "\n";
OS << " ]\n";
OS << "}\n";
}
int includeFixerMain(int argc, const char **argv) {
tooling::CommonOptionsParser options(argc, argv, IncludeFixerCategory);
tooling::ClangTool tool(options.getCompilations(),
options.getSourcePathList());
llvm::StringRef SourceFilePath = options.getSourcePathList().front();
// In STDINMode, we override the file content with the <stdin> input.
// Since `tool.mapVirtualFile` takes `StringRef`, we define `Code` outside of
// the if-block so that `Code` is not released after the if-block.
std::unique_ptr<llvm::MemoryBuffer> Code;
if (STDINMode) {
assert(options.getSourcePathList().size() == 1 &&
"Expect exactly one file path in STDINMode.");
llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> CodeOrErr =
MemoryBuffer::getSTDIN();
if (std::error_code EC = CodeOrErr.getError()) {
errs() << EC.message() << "\n";
return 1;
}
Code = std::move(CodeOrErr.get());
if (Code->getBufferSize() == 0)
return 0; // Skip empty files.
tool.mapVirtualFile(SourceFilePath, Code->getBuffer());
}
if (!InsertHeader.empty()) {
if (!STDINMode) {
errs() << "Should be running in STDIN mode\n";
return 1;
}
llvm::yaml::Input yin(InsertHeader);
IncludeFixerContext Context;
yin >> Context;
const auto &HeaderInfos = Context.getHeaderInfos();
assert(!HeaderInfos.empty());
// We only accept one unique header.
// Check all elements in HeaderInfos have the same header.
bool IsUniqueHeader = std::equal(
HeaderInfos.begin()+1, HeaderInfos.end(), HeaderInfos.begin(),
[](const IncludeFixerContext::HeaderInfo &LHS,
const IncludeFixerContext::HeaderInfo &RHS) {
return LHS.Header == RHS.Header;
});
if (!IsUniqueHeader) {
errs() << "Expect exactly one unique header.\n";
return 1;
}
// If a header has multiple symbols, we won't add the missing namespace
// qualifiers because we don't know which one is exactly used.
//
// Check whether all elements in HeaderInfos have the same qualified name.
bool IsUniqueQualifiedName = std::equal(
HeaderInfos.begin() + 1, HeaderInfos.end(), HeaderInfos.begin(),
[](const IncludeFixerContext::HeaderInfo &LHS,
const IncludeFixerContext::HeaderInfo &RHS) {
return LHS.QualifiedName == RHS.QualifiedName;
});
auto InsertStyle = format::getStyle("file", Context.getFilePath(), Style);
if (!InsertStyle) {
llvm::errs() << llvm::toString(InsertStyle.takeError()) << "\n";
return 1;
}
auto Replacements = clang::include_fixer::createIncludeFixerReplacements(
Code->getBuffer(), Context, *InsertStyle,
/*AddQualifiers=*/IsUniqueQualifiedName);
if (!Replacements) {
errs() << "Failed to create replacements: "
<< llvm::toString(Replacements.takeError()) << "\n";
return 1;
}
auto ChangedCode =
tooling::applyAllReplacements(Code->getBuffer(), *Replacements);
if (!ChangedCode) {
llvm::errs() << llvm::toString(ChangedCode.takeError()) << "\n";
return 1;
}
llvm::outs() << *ChangedCode;
return 0;
}
// Set up data source.
std::unique_ptr<include_fixer::SymbolIndexManager> SymbolIndexMgr =
createSymbolIndexManager(SourceFilePath);
if (!SymbolIndexMgr)
return 1;
// Query symbol mode.
if (!QuerySymbol.empty()) {
auto MatchedSymbols = SymbolIndexMgr->search(
QuerySymbol, /*IsNestedSearch=*/true, SourceFilePath);
for (auto &Symbol : MatchedSymbols) {
std::string HeaderPath = Symbol.getFilePath().str();
Symbol.SetFilePath(((HeaderPath[0] == '"' || HeaderPath[0] == '<')
? HeaderPath
: "\"" + HeaderPath + "\""));
}
// We leave an empty symbol range as we don't know the range of the symbol
// being queried in this mode. include-fixer won't add namespace qualifiers
// if the symbol range is empty, which also fits this case.
IncludeFixerContext::QuerySymbolInfo Symbol;
Symbol.RawIdentifier = QuerySymbol;
auto Context =
IncludeFixerContext(SourceFilePath, {Symbol}, MatchedSymbols);
writeToJson(llvm::outs(), Context);
return 0;
}
// Now run our tool.
std::vector<include_fixer::IncludeFixerContext> Contexts;
include_fixer::IncludeFixerActionFactory Factory(*SymbolIndexMgr, Contexts,
Style, MinimizeIncludePaths);
if (tool.run(&Factory) != 0) {
// We suppress all Clang diagnostics (because they would be wrong,
// include-fixer does custom recovery) but still want to give some feedback
// in case there was a compiler error we couldn't recover from. The most
// common case for this is a #include in the file that couldn't be found.
llvm::errs() << "Fatal compiler error occurred while parsing file!"
" (incorrect include paths?)\n";
return 1;
}
assert(!Contexts.empty());
if (OutputHeaders) {
// FIXME: Print contexts of all processing files instead of the first one.
writeToJson(llvm::outs(), Contexts.front());
return 0;
}
std::vector<tooling::Replacements> FixerReplacements;
for (const auto &Context : Contexts) {
StringRef FilePath = Context.getFilePath();
auto InsertStyle = format::getStyle("file", FilePath, Style);
if (!InsertStyle) {
llvm::errs() << llvm::toString(InsertStyle.takeError()) << "\n";
return 1;
}
auto Buffer = llvm::MemoryBuffer::getFile(FilePath);
if (!Buffer) {
errs() << "Couldn't open file: " + FilePath.str() + ": "
<< Buffer.getError().message() + "\n";
return 1;
}
auto Replacements = clang::include_fixer::createIncludeFixerReplacements(
Buffer.get()->getBuffer(), Context, *InsertStyle);
if (!Replacements) {
errs() << "Failed to create replacement: "
<< llvm::toString(Replacements.takeError()) << "\n";
return 1;
}
FixerReplacements.push_back(*Replacements);
}
if (!Quiet) {
for (const auto &Context : Contexts) {
if (!Context.getHeaderInfos().empty()) {
llvm::errs() << "Added #include "
<< Context.getHeaderInfos().front().Header << " for "
<< Context.getFilePath() << "\n";
}
}
}
if (STDINMode) {
assert(FixerReplacements.size() == 1);
auto ChangedCode = tooling::applyAllReplacements(Code->getBuffer(),
FixerReplacements.front());
if (!ChangedCode) {
llvm::errs() << llvm::toString(ChangedCode.takeError()) << "\n";
return 1;
}
llvm::outs() << *ChangedCode;
return 0;
}
// Set up a new source manager for applying the resulting replacements.
IntrusiveRefCntPtr<DiagnosticOptions> DiagOpts(new DiagnosticOptions);
DiagnosticsEngine Diagnostics(new DiagnosticIDs, &*DiagOpts);
TextDiagnosticPrinter DiagnosticPrinter(outs(), &*DiagOpts);
SourceManager SM(Diagnostics, tool.getFiles());
Diagnostics.setClient(&DiagnosticPrinter, false);
// Write replacements to disk.
Rewriter Rewrites(SM, LangOptions());
for (const auto &Replacement : FixerReplacements) {
if (!tooling::applyAllReplacements(Replacement, Rewrites)) {
llvm::errs() << "Failed to apply replacements.\n";
return 1;
}
}
return Rewrites.overwriteChangedFiles();
}
} // namespace
int main(int argc, const char **argv) {
return includeFixerMain(argc, argv);
}

View File

@@ -0,0 +1,65 @@
;;; clang-include-fixer-test.el --- unit tests for clang-include-fixer.el -*- lexical-binding: t; -*-
;;; Commentary:
;; Unit tests for clang-include-fixer.el.
;;; Code:
(require 'clang-include-fixer)
(require 'cc-mode)
(require 'ert)
(ert-deftest clang-include-fixer--insert-line ()
"Unit test for `clang-include-fixer--insert-line'."
(with-temp-buffer
(insert "aa\nab\nac\nad\n")
(let ((from (current-buffer)))
(with-temp-buffer
(insert "aa\nac\nad\n")
(let ((to (current-buffer)))
(should (clang-include-fixer--insert-line from to))
(should (equal (buffer-string) "aa\nab\nac\nad\n")))))
(should (equal (buffer-string) "aa\nab\nac\nad\n"))))
(ert-deftest clang-include-fixer--insert-line-diff-on-empty-line ()
"Unit test for `clang-include-fixer--insert-line'."
(with-temp-buffer
(insert "aa\nab\n\nac\nad\n")
(let ((from (current-buffer)))
(with-temp-buffer
(insert "aa\n\nac\nad\n")
(let ((to (current-buffer)))
(should (clang-include-fixer--insert-line from to))
(should (equal (buffer-string) "aa\nab\n\nac\nad\n")))))
(should (equal (buffer-string) "aa\nab\n\nac\nad\n"))))
(ert-deftest clang-include-fixer--symbol-at-point ()
"Unit test for `clang-include-fixer--symbol-at-point'."
(with-temp-buffer
(insert "a+bbb::cc")
(c++-mode)
(goto-char (point-min))
(should (equal (clang-include-fixer--symbol-at-point) "a"))
(forward-char)
;; Emacs treats the character immediately following a symbol as part of the
;; symbol.
(should (equal (clang-include-fixer--symbol-at-point) "a"))
(forward-char)
(should (equal (clang-include-fixer--symbol-at-point) "bbb::cc"))
(goto-char (point-max))
(should (equal (clang-include-fixer--symbol-at-point) "bbb::cc"))))
(ert-deftest clang-include-fixer--highlight ()
(with-temp-buffer
(insert "util::Status foo;\n")
(setq buffer-file-coding-system 'utf-8-unix)
(should (equal nil (clang-include-fixer--highlight
'((Range . ((Offset . 0) (Length . 0)))))))
(let ((overlay (clang-include-fixer--highlight
'((Range . ((Offset . 1) (Length . 12)))))))
(should (equal 2 (overlay-start overlay)))
(should (equal 14 (overlay-end overlay))))))
;;; clang-include-fixer-test.el ends here

View File

@@ -0,0 +1,446 @@
;;; clang-include-fixer.el --- Emacs integration of the clang include fixer -*- lexical-binding: t; -*-
;; Keywords: tools, c
;; Package-Requires: ((cl-lib "0.5") (json "1.2") (let-alist "1.0.4"))
;;; Commentary:
;; This package allows Emacs users to invoke the 'clang-include-fixer' within
;; Emacs. 'clang-include-fixer' provides an automated way of adding #include
;; directives for missing symbols in one translation unit, see
;; <http://clang.llvm.org/extra/include-fixer.html>.
;;; Code:
(require 'cl-lib)
(require 'json)
(require 'let-alist)
(defgroup clang-include-fixer nil
"Clang-based include fixer."
:group 'tools)
(defvar clang-include-fixer-add-include-hook nil
"A hook that will be called for every added include.
The first argument is the filename of the include, the second argument is
non-nil if the include is a system-header.")
(defcustom clang-include-fixer-executable
"clang-include-fixer"
"Location of the clang-include-fixer executable.
A string containing the name or the full path of the executable."
:group 'clang-include-fixer
:type '(file :must-match t)
:risky t)
(defcustom clang-include-fixer-input-format
'yaml
"Input format for clang-include-fixer.
This string is passed as -db argument to
`clang-include-fixer-executable'."
:group 'clang-include-fixer
:type '(radio
(const :tag "Hard-coded mapping" :fixed)
(const :tag "YAML" yaml)
(symbol :tag "Other"))
:risky t)
(defcustom clang-include-fixer-init-string
""
"Database initialization string for clang-include-fixer.
This string is passed as -input argument to
`clang-include-fixer-executable'."
:group 'clang-include-fixer
:type 'string
:risky t)
(defface clang-include-fixer-highlight '((t :background "green"))
"Used for highlighting the symbol for which a header file is being added.")
;;;###autoload
(defun clang-include-fixer ()
"Invoke the Include Fixer to insert missing C++ headers."
(interactive)
(message (concat "Calling the include fixer. "
"This might take some seconds. Please wait."))
(clang-include-fixer--start #'clang-include-fixer--add-header
"-output-headers"))
;;;###autoload
(defun clang-include-fixer-at-point ()
"Invoke the Clang include fixer for the symbol at point."
(interactive)
(let ((symbol (clang-include-fixer--symbol-at-point)))
(unless symbol
(user-error "No symbol at current location"))
(clang-include-fixer-from-symbol symbol)))
;;;###autoload
(defun clang-include-fixer-from-symbol (symbol)
"Invoke the Clang include fixer for the SYMBOL.
When called interactively, prompts the user for a symbol."
(interactive
(list (read-string "Symbol: " (clang-include-fixer--symbol-at-point))))
(clang-include-fixer--start #'clang-include-fixer--add-header
(format "-query-symbol=%s" symbol)))
(defun clang-include-fixer--start (callback &rest args)
"Asynchronously start clang-include-fixer with parameters ARGS.
The current file name is passed after ARGS as last argument. If
the call was successful the returned result is stored in a
temporary buffer, and CALLBACK is called with the temporary
buffer as only argument."
(unless buffer-file-name
(user-error "clang-include-fixer works only in buffers that visit a file"))
(let ((process (if (fboundp 'make-process)
;; Prefer using make-process if available, because
;; start-process doesnt allow us to separate the
;; standard error from the output.
(clang-include-fixer--make-process callback args)
(clang-include-fixer--start-process callback args))))
(save-restriction
(widen)
(process-send-region process (point-min) (point-max)))
(process-send-eof process))
nil)
(defun clang-include-fixer--make-process (callback args)
"Start a new clang-incude-fixer process using `make-process'.
CALLBACK is called after the process finishes successfully; it is
called with a single argument, the buffer where standard output
has been inserted. ARGS is a list of additional command line
arguments. Return the new process object."
(let ((stdin (current-buffer))
(stdout (generate-new-buffer "*clang-include-fixer output*"))
(stderr (generate-new-buffer "*clang-include-fixer errors*")))
(make-process :name "clang-include-fixer"
:buffer stdout
:command (clang-include-fixer--command args)
:coding 'utf-8-unix
:noquery t
:connection-type 'pipe
:sentinel (clang-include-fixer--sentinel stdin stdout stderr
callback)
:stderr stderr)))
(defun clang-include-fixer--start-process (callback args)
"Start a new clang-incude-fixer process using `start-process'.
CALLBACK is called after the process finishes successfully; it is
called with a single argument, the buffer where standard output
has been inserted. ARGS is a list of additional command line
arguments. Return the new process object."
(let* ((stdin (current-buffer))
(stdout (generate-new-buffer "*clang-include-fixer output*"))
(process-connection-type nil)
(process (apply #'start-process "clang-include-fixer" stdout
(clang-include-fixer--command args))))
(set-process-coding-system process 'utf-8-unix 'utf-8-unix)
(set-process-query-on-exit-flag process nil)
(set-process-sentinel process
(clang-include-fixer--sentinel stdin stdout nil
callback))
process))
(defun clang-include-fixer--command (args)
"Return the clang-include-fixer command line.
Returns a list; the first element is the binary to
execute (`clang-include-fixer-executable'), and the remaining
elements are the command line arguments. Adds proper arguments
for `clang-include-fixer-input-format' and
`clang-include-fixer-init-string'. Appends the current buffer's
file name; prepends ARGS directly in front of it."
(cl-check-type args list)
`(,clang-include-fixer-executable
,(format "-db=%s" clang-include-fixer-input-format)
,(format "-input=%s" clang-include-fixer-init-string)
"-stdin"
,@args
,(buffer-file-name)))
(defun clang-include-fixer--sentinel (stdin stdout stderr callback)
"Return a process sentinel for clang-include-fixer processes.
STDIN, STDOUT, and STDERR are buffers for the standard streams;
only STDERR may be nil. CALLBACK is called in the case of
success; it is called with a single argument, STDOUT. On
failure, a buffer containing the error output is displayed."
(cl-check-type stdin buffer-live)
(cl-check-type stdout buffer-live)
(cl-check-type stderr (or null buffer-live))
(cl-check-type callback function)
(lambda (process event)
(cl-check-type process process)
(cl-check-type event string)
(unwind-protect
(if (string-equal event "finished\n")
(progn
(when stderr (kill-buffer stderr))
(with-current-buffer stdin
(funcall callback stdout))
(kill-buffer stdout))
(when stderr (kill-buffer stdout))
(message "clang-include-fixer failed")
(with-current-buffer (or stderr stdout)
(insert "\nProcess " (process-name process)
?\s event))
(display-buffer (or stderr stdout))))
nil))
(defun clang-include-fixer--replace-buffer (stdout)
"Replace current buffer by content of STDOUT."
(cl-check-type stdout buffer-live)
(barf-if-buffer-read-only)
(cond ((fboundp 'replace-buffer-contents) (replace-buffer-contents stdout))
((clang-include-fixer--insert-line stdout (current-buffer)))
(t (erase-buffer) (insert-buffer-substring stdout)))
(message "Fix applied")
nil)
(defun clang-include-fixer--insert-line (from to)
"Insert a single missing line from the buffer FROM into TO.
FROM and TO must be buffers. If the contents of FROM and TO are
equal, do nothing and return non-nil. If FROM contains a single
line missing from TO, insert that line into TO so that the buffer
contents are equal and return non-nil. Otherwise, do nothing and
return nil. Buffer restrictions are ignored."
(cl-check-type from buffer-live)
(cl-check-type to buffer-live)
(with-current-buffer from
(save-excursion
(save-restriction
(widen)
(with-current-buffer to
(save-excursion
(save-restriction
(widen)
;; Search for the first buffer difference.
(let ((chars (abs (compare-buffer-substrings to nil nil from nil nil))))
(if (zerop chars)
;; Buffer contents are equal, nothing to do.
t
(goto-char chars)
;; We might have ended up in the middle of a line if the
;; current line partially matches. In this case we would
;; have to insert more than a line. Move to the beginning of
;; the line to avoid this situation.
(beginning-of-line)
(with-current-buffer from
(goto-char chars)
(beginning-of-line)
(let ((from-begin (point))
(from-end (progn (forward-line) (point)))
(to-point (with-current-buffer to (point))))
;; Search for another buffer difference after the line in
;; question. If there is none, we can proceed.
(when (zerop (compare-buffer-substrings from from-end nil
to to-point nil))
(with-current-buffer to
(insert-buffer-substring from from-begin from-end))
t))))))))))))
(defun clang-include-fixer--add-header (stdout)
"Analyse the result of include-fixer stored in STDOUT.
Add a missing header if there is any. If there are multiple
possible headers the user can select one of them to be included.
Temporarily highlight the affected symbols. Asynchronously call
clang-include-fixer to insert the selected header."
(cl-check-type stdout buffer-live)
(let ((context (clang-include-fixer--parse-json stdout)))
(let-alist context
(cond
((null .QuerySymbolInfos)
(message "The file is fine, no need to add a header."))
((null .HeaderInfos)
(message "Couldn't find header for '%s'"
(let-alist (car .QuerySymbolInfos) .RawIdentifier)))
(t
;; Users may C-g in prompts, make sure the process sentinel
;; behaves correctly.
(with-local-quit
;; Replace the HeaderInfos list by a single header selected by
;; the user.
(clang-include-fixer--select-header context)
;; Call clang-include-fixer again to insert the selected header.
(clang-include-fixer--start
(let ((old-tick (buffer-chars-modified-tick)))
(lambda (stdout)
(when (/= old-tick (buffer-chars-modified-tick))
;; Replacing the buffer now would undo the users changes.
(user-error (concat "The buffer has been changed "
"before the header could be inserted")))
(clang-include-fixer--replace-buffer stdout)
(let-alist context
(let-alist (car .HeaderInfos)
(with-local-quit
(run-hook-with-args 'clang-include-fixer-add-include-hook
(substring .Header 1 -1)
(string= (substring .Header 0 1) "<")))))))
(format "-insert-header=%s"
(clang-include-fixer--encode-json context))))))))
nil)
(defun clang-include-fixer--select-header (context)
"Prompt the user for a header if necessary.
CONTEXT must be a clang-include-fixer context object in
association list format. If it contains more than one HeaderInfo
element, prompt the user to select one of the headers. CONTEXT
is modified to include only the selected element."
(cl-check-type context cons)
(let-alist context
(if (cdr .HeaderInfos)
(clang-include-fixer--prompt-for-header context)
(message "Only one include is missing: %s"
(let-alist (car .HeaderInfos) .Header))))
nil)
(defvar clang-include-fixer--history nil
"History for `clang-include-fixer--prompt-for-header'.")
(defun clang-include-fixer--prompt-for-header (context)
"Prompt the user for a single header.
The choices are taken from the HeaderInfo elements in CONTEXT.
They are replaced by the single element selected by the user."
(let-alist context
(let ((symbol (clang-include-fixer--symbol-name .QuerySymbolInfos))
;; Add temporary highlighting so that the user knows which
;; symbols the current session is about.
(overlays (remove nil
(mapcar #'clang-include-fixer--highlight .QuerySymbolInfos))))
(unwind-protect
(save-excursion
;; While prompting, go to the closest overlay so that the user sees
;; some context.
(when overlays
(goto-char (clang-include-fixer--closest-overlay overlays)))
(cl-flet ((header (info) (let-alist info .Header)))
;; The header-infos is already sorted by include-fixer.
(let* ((header (completing-read
(clang-include-fixer--format-message
"Select include for '%s': " symbol)
(mapcar #'header .HeaderInfos)
nil :require-match nil
'clang-include-fixer--history))
(info (cl-find header .HeaderInfos :key #'header :test #'string=)))
(cl-assert info)
(setcar .HeaderInfos info)
(setcdr .HeaderInfos nil))))
(mapc #'delete-overlay overlays)))))
(defun clang-include-fixer--symbol-name (symbol-infos)
"Return the unique symbol name in SYMBOL-INFOS.
Raise a signal if the symbol name is not unique."
(let ((symbols (delete-dups (mapcar (lambda (info)
(let-alist info .RawIdentifier))
symbol-infos))))
(when (cdr symbols)
(error "Multiple symbols %s returned" symbols))
(car symbols)))
(defun clang-include-fixer--highlight (symbol-info)
"Add an overlay to highlight SYMBOL-INFO, if it points to a non-empty range.
Return the overlay object, or nil."
(let-alist symbol-info
(unless (zerop .Range.Length)
(let ((overlay (make-overlay
(clang-include-fixer--filepos-to-bufferpos
.Range.Offset 'approximate)
(clang-include-fixer--filepos-to-bufferpos
(+ .Range.Offset .Range.Length) 'approximate))))
(overlay-put overlay 'face 'clang-include-fixer-highlight)
overlay))))
(defun clang-include-fixer--closest-overlay (overlays)
"Return the start of the overlay in OVERLAYS that is closest to point."
(cl-check-type overlays cons)
(let ((point (point))
acc)
(dolist (overlay overlays acc)
(let ((start (overlay-start overlay)))
(when (or (null acc) (< (abs (- point start)) (abs (- point acc))))
(setq acc start))))))
(defun clang-include-fixer--parse-json (buffer)
"Parse a JSON response from clang-include-fixer in BUFFER.
Return the JSON object as an association list."
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(let ((json-object-type 'alist)
(json-array-type 'list)
(json-key-type 'symbol)
(json-false :json-false)
(json-null nil)
(json-pre-element-read-function nil)
(json-post-element-read-function nil))
(json-read)))))
(defun clang-include-fixer--encode-json (object)
"Return the JSON representation of OBJECT as a string."
(let ((json-encoding-separator ",")
(json-encoding-default-indentation " ")
(json-encoding-pretty-print nil)
(json-encoding-lisp-style-closings nil)
(json-encoding-object-sort-predicate nil))
(json-encode object)))
(defun clang-include-fixer--symbol-at-point ()
"Return the qualified symbol at point.
If there is no symbol at point, return nil."
;; Let bounds-of-thing-at-point to do the hard work and deal with edge
;; cases.
(let ((bounds (bounds-of-thing-at-point 'symbol)))
(when bounds
(let ((beg (car bounds))
(end (cdr bounds)))
(save-excursion
;; Extend the symbol range to the left. Skip over namespace
;; delimiters and parent namespace names.
(goto-char beg)
(while (and (clang-include-fixer--skip-double-colon-backward)
(skip-syntax-backward "w_")))
;; Skip over one more namespace delimiter, for absolute names.
(clang-include-fixer--skip-double-colon-backward)
(setq beg (point))
;; Extend the symbol range to the right. Skip over namespace
;; delimiters and child namespace names.
(goto-char end)
(while (and (clang-include-fixer--skip-double-colon-forward)
(skip-syntax-forward "w_")))
(setq end (point)))
(buffer-substring-no-properties beg end)))))
(defun clang-include-fixer--skip-double-colon-forward ()
"Skip a double colon.
When the next two characters are '::', skip them and return
non-nil. Otherwise return nil."
(let ((end (+ (point) 2)))
(when (and (<= end (point-max))
(string-equal (buffer-substring-no-properties (point) end) "::"))
(goto-char end)
t)))
(defun clang-include-fixer--skip-double-colon-backward ()
"Skip a double colon.
When the previous two characters are '::', skip them and return
non-nil. Otherwise return nil."
(let ((beg (- (point) 2)))
(when (and (>= beg (point-min))
(string-equal (buffer-substring-no-properties beg (point)) "::"))
(goto-char beg)
t)))
;; filepos-to-bufferpos is new in Emacs 25.1. Provide a fallback for older
;; versions.
(defalias 'clang-include-fixer--filepos-to-bufferpos
(if (fboundp 'filepos-to-bufferpos)
'filepos-to-bufferpos
(lambda (byte &optional _quality _coding-system)
(byte-to-position (1+ byte)))))
;; format-message is new in Emacs 25.1. Provide a fallback for older
;; versions.
(defalias 'clang-include-fixer--format-message
(if (fboundp 'format-message) 'format-message 'format))
(provide 'clang-include-fixer)
;;; clang-include-fixer.el ends here

View File

@@ -0,0 +1,208 @@
# This file is a minimal clang-include-fixer vim-integration. To install:
# - Change 'binary' if clang-include-fixer is not on the path (see below).
# - Add to your .vimrc:
#
# noremap <leader>cf :pyf path/to/llvm/source/tools/clang/tools/extra/include-fixer/tool/clang-include-fixer.py<cr>
#
# This enables clang-include-fixer for NORMAL and VISUAL mode. Change "<leader>cf"
# to another binding if you need clang-include-fixer on a different key.
#
# To set up clang-include-fixer, see http://clang.llvm.org/extra/include-fixer.html
#
# With this integration you can press the bound key and clang-include-fixer will
# be run on the current buffer.
#
# It operates on the current, potentially unsaved buffer and does not create
# or save any files. To revert a fix, just undo.
import argparse
import difflib
import json
import re
import subprocess
import vim
# set g:clang_include_fixer_path to the path to clang-include-fixer if it is not
# on the path.
# Change this to the full path if clang-include-fixer is not on the path.
binary = 'clang-include-fixer'
if vim.eval('exists("g:clang_include_fixer_path")') == "1":
binary = vim.eval('g:clang_include_fixer_path')
maximum_suggested_headers = 3
if vim.eval('exists("g:clang_include_fixer_maximum_suggested_headers")') == "1":
maximum_suggested_headers = max(
1,
vim.eval('g:clang_include_fixer_maximum_suggested_headers'))
increment_num = 5
if vim.eval('exists("g:clang_include_fixer_increment_num")') == "1":
increment_num = max(
1,
vim.eval('g:clang_include_fixer_increment_num'))
jump_to_include = False
if vim.eval('exists("g:clang_include_fixer_jump_to_include")') == "1":
jump_to_include = vim.eval('g:clang_include_fixer_jump_to_include') != "0"
query_mode = False
if vim.eval('exists("g:clang_include_fixer_query_mode")') == "1":
query_mode = vim.eval('g:clang_include_fixer_query_mode') != "0"
def GetUserSelection(message, headers, maximum_suggested_headers):
eval_message = message + '\n'
for idx, header in enumerate(headers[0:maximum_suggested_headers]):
eval_message += "({0}). {1}\n".format(idx + 1, header)
eval_message += "Enter (q) to quit;"
if maximum_suggested_headers < len(headers):
eval_message += " (m) to show {0} more candidates.".format(
min(increment_num, len(headers) - maximum_suggested_headers))
eval_message += "\nSelect (default 1): "
res = vim.eval("input('{0}')".format(eval_message))
if res == '':
# choose the top ranked header by default
idx = 1
elif res == 'q':
raise Exception(' Insertion cancelled...')
elif res == 'm':
return GetUserSelection(message,
headers, maximum_suggested_headers + increment_num)
else:
try:
idx = int(res)
if idx <= 0 or idx > len(headers):
raise Exception()
except Exception:
# Show a new prompt on invalid option instead of aborting so that users
# don't need to wait for another include-fixer run.
print >> sys.stderr, "Invalid option:", res
return GetUserSelection(message, headers, maximum_suggested_headers)
return headers[idx - 1]
def execute(command, text):
p = subprocess.Popen(command,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
return p.communicate(input=text)
def InsertHeaderToVimBuffer(header, text):
command = [binary, "-stdin", "-insert-header=" + json.dumps(header),
vim.current.buffer.name]
stdout, stderr = execute(command, text)
if stderr:
raise Exception(stderr)
if stdout:
lines = stdout.splitlines()
sequence = difflib.SequenceMatcher(None, vim.current.buffer, lines)
line_num = None
for op in reversed(sequence.get_opcodes()):
if op[0] != 'equal':
vim.current.buffer[op[1]:op[2]] = lines[op[3]:op[4]]
if op[0] == 'insert':
# line_num in vim is 1-based.
line_num = op[1] + 1
if jump_to_include and line_num:
vim.current.window.cursor = (line_num, 0)
# The vim internal implementation (expand("cword"/"cWORD")) doesn't support
# our use case very well, we re-implement our own one.
def get_symbol_under_cursor():
line = vim.eval("line(\".\")")
# column number in vim is 1-based.
col = int(vim.eval("col(\".\")")) - 1
line_text = vim.eval("getline({0})".format(line))
if len(line_text) == 0: return ""
symbol_pos_begin = col
p = re.compile('[a-zA-Z0-9:_]')
while symbol_pos_begin >= 0 and p.match(line_text[symbol_pos_begin]):
symbol_pos_begin -= 1
symbol_pos_end = col
while symbol_pos_end < len(line_text) and p.match(line_text[symbol_pos_end]):
symbol_pos_end += 1
return line_text[symbol_pos_begin+1:symbol_pos_end]
def main():
parser = argparse.ArgumentParser(
description='Vim integration for clang-include-fixer')
parser.add_argument('-db', default='yaml',
help='clang-include-fixer input format.')
parser.add_argument('-input', default='',
help='String to initialize the database.')
# Don't throw exception when parsing unknown arguements to make the script
# work in neovim.
# Neovim (at least v0.2.1) somehow mangles the sys.argv in a weird way: it
# will pass additional arguments (e.g. "-c script_host.py") to sys.argv,
# which makes the script fail.
args, _ = parser.parse_known_args()
# Get the current text.
buf = vim.current.buffer
text = '\n'.join(buf)
if query_mode:
symbol = get_symbol_under_cursor()
if len(symbol) == 0:
print "Skip querying empty symbol."
return
command = [binary, "-stdin", "-query-symbol="+get_symbol_under_cursor(),
"-db=" + args.db, "-input=" + args.input,
vim.current.buffer.name]
else:
# Run command to get all headers.
command = [binary, "-stdin", "-output-headers", "-db=" + args.db,
"-input=" + args.input, vim.current.buffer.name]
stdout, stderr = execute(command, text)
if stderr:
print >> sys.stderr, "Error while running clang-include-fixer: " + stderr
return
include_fixer_context = json.loads(stdout)
query_symbol_infos = include_fixer_context["QuerySymbolInfos"]
if not query_symbol_infos:
print "The file is fine, no need to add a header."
return
symbol = query_symbol_infos[0]["RawIdentifier"]
# The header_infos is already sorted by include-fixer.
header_infos = include_fixer_context["HeaderInfos"]
# Deduplicate headers while keeping the order, so that the same header would
# not be suggested twice.
unique_headers = []
seen = set()
for header_info in header_infos:
header = header_info["Header"]
if header not in seen:
seen.add(header)
unique_headers.append(header)
if not unique_headers:
print "Couldn't find a header for {0}.".format(symbol)
return
try:
selected = unique_headers[0]
inserted_header_infos = header_infos
if len(unique_headers) > 1:
selected = GetUserSelection(
"choose a header file for {0}.".format(symbol),
unique_headers, maximum_suggested_headers)
inserted_header_infos = [
header for header in header_infos if header["Header"] == selected]
include_fixer_context["HeaderInfos"] = inserted_header_infos
InsertHeaderToVimBuffer(include_fixer_context, text)
print "Added #include {0} for {1}.".format(selected, symbol)
except Exception as error:
print >> sys.stderr, error.message
return
if __name__ == '__main__':
main()