]>
Commit | Line | Data |
---|---|---|
223e47cc LB |
1 | //===--- HTMLDiagnostics.cpp - HTML Diagnostics for Paths ----*- C++ -*-===// |
2 | // | |
3 | // The LLVM Compiler Infrastructure | |
4 | // | |
5 | // This file is distributed under the University of Illinois Open Source | |
6 | // License. See LICENSE.TXT for details. | |
7 | // | |
8 | //===----------------------------------------------------------------------===// | |
9 | // | |
10 | // This file defines the HTMLDiagnostics object. | |
11 | // | |
12 | //===----------------------------------------------------------------------===// | |
13 | ||
14 | #include "clang/StaticAnalyzer/Core/PathDiagnosticConsumers.h" | |
15 | #include "clang/StaticAnalyzer/Core/BugReporter/PathDiagnostic.h" | |
16 | #include "clang/AST/ASTContext.h" | |
17 | #include "clang/AST/Decl.h" | |
18 | #include "clang/Basic/SourceManager.h" | |
19 | #include "clang/Basic/FileManager.h" | |
20 | #include "clang/Rewrite/Core/Rewriter.h" | |
21 | #include "clang/Rewrite/Core/HTMLRewrite.h" | |
22 | #include "clang/Lex/Lexer.h" | |
23 | #include "clang/Lex/Preprocessor.h" | |
24 | #include "llvm/Support/FileSystem.h" | |
25 | #include "llvm/Support/MemoryBuffer.h" | |
26 | #include "llvm/Support/raw_ostream.h" | |
27 | #include "llvm/Support/Path.h" | |
28 | ||
29 | using namespace clang; | |
30 | using namespace ento; | |
31 | ||
32 | //===----------------------------------------------------------------------===// | |
33 | // Boilerplate. | |
34 | //===----------------------------------------------------------------------===// | |
35 | ||
36 | namespace { | |
37 | ||
38 | class HTMLDiagnostics : public PathDiagnosticConsumer { | |
39 | llvm::sys::Path Directory, FilePrefix; | |
40 | bool createdDir, noDir; | |
41 | const Preprocessor &PP; | |
42 | public: | |
43 | HTMLDiagnostics(const std::string& prefix, const Preprocessor &pp); | |
44 | ||
45 | virtual ~HTMLDiagnostics() { FlushDiagnostics(NULL); } | |
46 | ||
47 | virtual void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags, | |
48 | FilesMade *filesMade); | |
49 | ||
50 | virtual StringRef getName() const { | |
51 | return "HTMLDiagnostics"; | |
52 | } | |
53 | ||
54 | unsigned ProcessMacroPiece(raw_ostream &os, | |
55 | const PathDiagnosticMacroPiece& P, | |
56 | unsigned num); | |
57 | ||
58 | void HandlePiece(Rewriter& R, FileID BugFileID, | |
59 | const PathDiagnosticPiece& P, unsigned num, unsigned max); | |
60 | ||
61 | void HighlightRange(Rewriter& R, FileID BugFileID, SourceRange Range, | |
62 | const char *HighlightStart = "<span class=\"mrange\">", | |
63 | const char *HighlightEnd = "</span>"); | |
64 | ||
65 | void ReportDiag(const PathDiagnostic& D, | |
66 | FilesMade *filesMade); | |
67 | }; | |
68 | ||
69 | } // end anonymous namespace | |
70 | ||
71 | HTMLDiagnostics::HTMLDiagnostics(const std::string& prefix, | |
72 | const Preprocessor &pp) | |
73 | : Directory(prefix), FilePrefix(prefix), createdDir(false), noDir(false), | |
74 | PP(pp) { | |
75 | // All html files begin with "report" | |
76 | FilePrefix.appendComponent("report"); | |
77 | } | |
78 | ||
79 | void ento::createHTMLDiagnosticConsumer(PathDiagnosticConsumers &C, | |
80 | const std::string& prefix, | |
81 | const Preprocessor &PP) { | |
82 | C.push_back(new HTMLDiagnostics(prefix, PP)); | |
83 | } | |
84 | ||
85 | //===----------------------------------------------------------------------===// | |
86 | // Report processing. | |
87 | //===----------------------------------------------------------------------===// | |
88 | ||
89 | void HTMLDiagnostics::FlushDiagnosticsImpl( | |
90 | std::vector<const PathDiagnostic *> &Diags, | |
91 | FilesMade *filesMade) { | |
92 | for (std::vector<const PathDiagnostic *>::iterator it = Diags.begin(), | |
93 | et = Diags.end(); it != et; ++it) { | |
94 | ReportDiag(**it, filesMade); | |
95 | } | |
96 | } | |
97 | ||
98 | void HTMLDiagnostics::ReportDiag(const PathDiagnostic& D, | |
99 | FilesMade *filesMade) { | |
100 | ||
101 | // Create the HTML directory if it is missing. | |
102 | if (!createdDir) { | |
103 | createdDir = true; | |
104 | std::string ErrorMsg; | |
105 | Directory.createDirectoryOnDisk(true, &ErrorMsg); | |
106 | ||
107 | bool IsDirectory; | |
108 | if (llvm::sys::fs::is_directory(Directory.str(), IsDirectory) || | |
109 | !IsDirectory) { | |
110 | llvm::errs() << "warning: could not create directory '" | |
111 | << Directory.str() << "'\n" | |
112 | << "reason: " << ErrorMsg << '\n'; | |
113 | ||
114 | noDir = true; | |
115 | ||
116 | return; | |
117 | } | |
118 | } | |
119 | ||
120 | if (noDir) | |
121 | return; | |
122 | ||
123 | // First flatten out the entire path to make it easier to use. | |
124 | PathPieces path = D.path.flatten(/*ShouldFlattenMacros=*/false); | |
125 | ||
126 | // The path as already been prechecked that all parts of the path are | |
127 | // from the same file and that it is non-empty. | |
128 | const SourceManager &SMgr = (*path.begin())->getLocation().getManager(); | |
129 | assert(!path.empty()); | |
130 | FileID FID = | |
131 | (*path.begin())->getLocation().asLocation().getExpansionLoc().getFileID(); | |
132 | assert(!FID.isInvalid()); | |
133 | ||
134 | // Create a new rewriter to generate HTML. | |
135 | Rewriter R(const_cast<SourceManager&>(SMgr), PP.getLangOpts()); | |
136 | ||
137 | // Process the path. | |
138 | unsigned n = path.size(); | |
139 | unsigned max = n; | |
140 | ||
141 | for (PathPieces::const_reverse_iterator I = path.rbegin(), | |
142 | E = path.rend(); | |
143 | I != E; ++I, --n) | |
144 | HandlePiece(R, FID, **I, n, max); | |
145 | ||
146 | // Add line numbers, header, footer, etc. | |
147 | ||
148 | // unsigned FID = R.getSourceMgr().getMainFileID(); | |
149 | html::EscapeText(R, FID); | |
150 | html::AddLineNumbers(R, FID); | |
151 | ||
152 | // If we have a preprocessor, relex the file and syntax highlight. | |
153 | // We might not have a preprocessor if we come from a deserialized AST file, | |
154 | // for example. | |
155 | ||
156 | html::SyntaxHighlight(R, FID, PP); | |
157 | html::HighlightMacros(R, FID, PP); | |
158 | ||
159 | // Get the full directory name of the analyzed file. | |
160 | ||
161 | const FileEntry* Entry = SMgr.getFileEntryForID(FID); | |
162 | ||
163 | // This is a cludge; basically we want to append either the full | |
164 | // working directory if we have no directory information. This is | |
165 | // a work in progress. | |
166 | ||
167 | std::string DirName = ""; | |
168 | ||
169 | if (llvm::sys::path::is_relative(Entry->getName())) { | |
170 | llvm::sys::Path P = llvm::sys::Path::GetCurrentDirectory(); | |
171 | DirName = P.str() + "/"; | |
172 | } | |
173 | ||
174 | // Add the name of the file as an <h1> tag. | |
175 | ||
176 | { | |
177 | std::string s; | |
178 | llvm::raw_string_ostream os(s); | |
179 | ||
180 | os << "<!-- REPORTHEADER -->\n" | |
181 | << "<h3>Bug Summary</h3>\n<table class=\"simpletable\">\n" | |
182 | "<tr><td class=\"rowname\">File:</td><td>" | |
183 | << html::EscapeText(DirName) | |
184 | << html::EscapeText(Entry->getName()) | |
185 | << "</td></tr>\n<tr><td class=\"rowname\">Location:</td><td>" | |
186 | "<a href=\"#EndPath\">line " | |
187 | << (*path.rbegin())->getLocation().asLocation().getExpansionLineNumber() | |
188 | << ", column " | |
189 | << (*path.rbegin())->getLocation().asLocation().getExpansionColumnNumber() | |
190 | << "</a></td></tr>\n" | |
191 | "<tr><td class=\"rowname\">Description:</td><td>" | |
192 | << D.getVerboseDescription() << "</td></tr>\n"; | |
193 | ||
194 | // Output any other meta data. | |
195 | ||
196 | for (PathDiagnostic::meta_iterator I=D.meta_begin(), E=D.meta_end(); | |
197 | I!=E; ++I) { | |
198 | os << "<tr><td></td><td>" << html::EscapeText(*I) << "</td></tr>\n"; | |
199 | } | |
200 | ||
201 | os << "</table>\n<!-- REPORTSUMMARYEXTRA -->\n" | |
202 | "<h3>Annotated Source Code</h3>\n"; | |
203 | ||
204 | R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str()); | |
205 | } | |
206 | ||
207 | // Embed meta-data tags. | |
208 | { | |
209 | std::string s; | |
210 | llvm::raw_string_ostream os(s); | |
211 | ||
212 | StringRef BugDesc = D.getVerboseDescription(); | |
213 | if (!BugDesc.empty()) | |
214 | os << "\n<!-- BUGDESC " << BugDesc << " -->\n"; | |
215 | ||
216 | StringRef BugType = D.getBugType(); | |
217 | if (!BugType.empty()) | |
218 | os << "\n<!-- BUGTYPE " << BugType << " -->\n"; | |
219 | ||
220 | StringRef BugCategory = D.getCategory(); | |
221 | if (!BugCategory.empty()) | |
222 | os << "\n<!-- BUGCATEGORY " << BugCategory << " -->\n"; | |
223 | ||
224 | os << "\n<!-- BUGFILE " << DirName << Entry->getName() << " -->\n"; | |
225 | ||
226 | os << "\n<!-- BUGLINE " | |
227 | << path.back()->getLocation().asLocation().getExpansionLineNumber() | |
228 | << " -->\n"; | |
229 | ||
230 | os << "\n<!-- BUGPATHLENGTH " << path.size() << " -->\n"; | |
231 | ||
232 | // Mark the end of the tags. | |
233 | os << "\n<!-- BUGMETAEND -->\n"; | |
234 | ||
235 | // Insert the text. | |
236 | R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str()); | |
237 | } | |
238 | ||
239 | // Add CSS, header, and footer. | |
240 | ||
241 | html::AddHeaderFooterInternalBuiltinCSS(R, FID, Entry->getName()); | |
242 | ||
243 | // Get the rewrite buffer. | |
244 | const RewriteBuffer *Buf = R.getRewriteBufferFor(FID); | |
245 | ||
246 | if (!Buf) { | |
247 | llvm::errs() << "warning: no diagnostics generated for main file.\n"; | |
248 | return; | |
249 | } | |
250 | ||
251 | // Create a path for the target HTML file. | |
252 | llvm::sys::Path F(FilePrefix); | |
253 | F.makeUnique(false, NULL); | |
254 | ||
255 | // Rename the file with an HTML extension. | |
256 | llvm::sys::Path H(F); | |
257 | H.appendSuffix("html"); | |
258 | F.renamePathOnDisk(H, NULL); | |
259 | ||
260 | std::string ErrorMsg; | |
261 | llvm::raw_fd_ostream os(H.c_str(), ErrorMsg); | |
262 | ||
263 | if (!ErrorMsg.empty()) { | |
264 | llvm::errs() << "warning: could not create file '" << F.str() | |
265 | << "'\n"; | |
266 | return; | |
267 | } | |
268 | ||
269 | if (filesMade) { | |
270 | filesMade->addDiagnostic(D, getName(), llvm::sys::path::filename(H.str())); | |
271 | } | |
272 | ||
273 | // Emit the HTML to disk. | |
274 | for (RewriteBuffer::iterator I = Buf->begin(), E = Buf->end(); I!=E; ++I) | |
275 | os << *I; | |
276 | } | |
277 | ||
278 | void HTMLDiagnostics::HandlePiece(Rewriter& R, FileID BugFileID, | |
279 | const PathDiagnosticPiece& P, | |
280 | unsigned num, unsigned max) { | |
281 | ||
282 | // For now, just draw a box above the line in question, and emit the | |
283 | // warning. | |
284 | FullSourceLoc Pos = P.getLocation().asLocation(); | |
285 | ||
286 | if (!Pos.isValid()) | |
287 | return; | |
288 | ||
289 | SourceManager &SM = R.getSourceMgr(); | |
290 | assert(&Pos.getManager() == &SM && "SourceManagers are different!"); | |
291 | std::pair<FileID, unsigned> LPosInfo = SM.getDecomposedExpansionLoc(Pos); | |
292 | ||
293 | if (LPosInfo.first != BugFileID) | |
294 | return; | |
295 | ||
296 | const llvm::MemoryBuffer *Buf = SM.getBuffer(LPosInfo.first); | |
297 | const char* FileStart = Buf->getBufferStart(); | |
298 | ||
299 | // Compute the column number. Rewind from the current position to the start | |
300 | // of the line. | |
301 | unsigned ColNo = SM.getColumnNumber(LPosInfo.first, LPosInfo.second); | |
302 | const char *TokInstantiationPtr =Pos.getExpansionLoc().getCharacterData(); | |
303 | const char *LineStart = TokInstantiationPtr-ColNo; | |
304 | ||
305 | // Compute LineEnd. | |
306 | const char *LineEnd = TokInstantiationPtr; | |
307 | const char* FileEnd = Buf->getBufferEnd(); | |
308 | while (*LineEnd != '\n' && LineEnd != FileEnd) | |
309 | ++LineEnd; | |
310 | ||
311 | // Compute the margin offset by counting tabs and non-tabs. | |
312 | unsigned PosNo = 0; | |
313 | for (const char* c = LineStart; c != TokInstantiationPtr; ++c) | |
314 | PosNo += *c == '\t' ? 8 : 1; | |
315 | ||
316 | // Create the html for the message. | |
317 | ||
318 | const char *Kind = 0; | |
319 | switch (P.getKind()) { | |
320 | case PathDiagnosticPiece::Call: | |
321 | llvm_unreachable("Calls should already be handled"); | |
322 | case PathDiagnosticPiece::Event: Kind = "Event"; break; | |
323 | case PathDiagnosticPiece::ControlFlow: Kind = "Control"; break; | |
324 | // Setting Kind to "Control" is intentional. | |
325 | case PathDiagnosticPiece::Macro: Kind = "Control"; break; | |
326 | } | |
327 | ||
328 | std::string sbuf; | |
329 | llvm::raw_string_ostream os(sbuf); | |
330 | ||
331 | os << "\n<tr><td class=\"num\"></td><td class=\"line\"><div id=\""; | |
332 | ||
333 | if (num == max) | |
334 | os << "EndPath"; | |
335 | else | |
336 | os << "Path" << num; | |
337 | ||
338 | os << "\" class=\"msg"; | |
339 | if (Kind) | |
340 | os << " msg" << Kind; | |
341 | os << "\" style=\"margin-left:" << PosNo << "ex"; | |
342 | ||
343 | // Output a maximum size. | |
344 | if (!isa<PathDiagnosticMacroPiece>(P)) { | |
345 | // Get the string and determining its maximum substring. | |
346 | const std::string& Msg = P.getString(); | |
347 | unsigned max_token = 0; | |
348 | unsigned cnt = 0; | |
349 | unsigned len = Msg.size(); | |
350 | ||
351 | for (std::string::const_iterator I=Msg.begin(), E=Msg.end(); I!=E; ++I) | |
352 | switch (*I) { | |
353 | default: | |
354 | ++cnt; | |
355 | continue; | |
356 | case ' ': | |
357 | case '\t': | |
358 | case '\n': | |
359 | if (cnt > max_token) max_token = cnt; | |
360 | cnt = 0; | |
361 | } | |
362 | ||
363 | if (cnt > max_token) | |
364 | max_token = cnt; | |
365 | ||
366 | // Determine the approximate size of the message bubble in em. | |
367 | unsigned em; | |
368 | const unsigned max_line = 120; | |
369 | ||
370 | if (max_token >= max_line) | |
371 | em = max_token / 2; | |
372 | else { | |
373 | unsigned characters = max_line; | |
374 | unsigned lines = len / max_line; | |
375 | ||
376 | if (lines > 0) { | |
377 | for (; characters > max_token; --characters) | |
378 | if (len / characters > lines) { | |
379 | ++characters; | |
380 | break; | |
381 | } | |
382 | } | |
383 | ||
384 | em = characters / 2; | |
385 | } | |
386 | ||
387 | if (em < max_line/2) | |
388 | os << "; max-width:" << em << "em"; | |
389 | } | |
390 | else | |
391 | os << "; max-width:100em"; | |
392 | ||
393 | os << "\">"; | |
394 | ||
395 | if (max > 1) { | |
396 | os << "<table class=\"msgT\"><tr><td valign=\"top\">"; | |
397 | os << "<div class=\"PathIndex"; | |
398 | if (Kind) os << " PathIndex" << Kind; | |
399 | os << "\">" << num << "</div>"; | |
400 | ||
401 | if (num > 1) { | |
402 | os << "</td><td><div class=\"PathNav\"><a href=\"#Path" | |
403 | << (num - 1) | |
404 | << "\" title=\"Previous event (" | |
405 | << (num - 1) | |
406 | << ")\">←</a></div></td>"; | |
407 | } | |
408 | ||
409 | os << "</td><td>"; | |
410 | } | |
411 | ||
412 | if (const PathDiagnosticMacroPiece *MP = | |
413 | dyn_cast<PathDiagnosticMacroPiece>(&P)) { | |
414 | ||
415 | os << "Within the expansion of the macro '"; | |
416 | ||
417 | // Get the name of the macro by relexing it. | |
418 | { | |
419 | FullSourceLoc L = MP->getLocation().asLocation().getExpansionLoc(); | |
420 | assert(L.isFileID()); | |
421 | StringRef BufferInfo = L.getBufferData(); | |
422 | std::pair<FileID, unsigned> LocInfo = L.getDecomposedLoc(); | |
423 | const char* MacroName = LocInfo.second + BufferInfo.data(); | |
424 | Lexer rawLexer(SM.getLocForStartOfFile(LocInfo.first), PP.getLangOpts(), | |
425 | BufferInfo.begin(), MacroName, BufferInfo.end()); | |
426 | ||
427 | Token TheTok; | |
428 | rawLexer.LexFromRawLexer(TheTok); | |
429 | for (unsigned i = 0, n = TheTok.getLength(); i < n; ++i) | |
430 | os << MacroName[i]; | |
431 | } | |
432 | ||
433 | os << "':\n"; | |
434 | ||
435 | if (max > 1) { | |
436 | os << "</td>"; | |
437 | if (num < max) { | |
438 | os << "<td><div class=\"PathNav\"><a href=\"#"; | |
439 | if (num == max - 1) | |
440 | os << "EndPath"; | |
441 | else | |
442 | os << "Path" << (num + 1); | |
443 | os << "\" title=\"Next event (" | |
444 | << (num + 1) | |
445 | << ")\">→</a></div></td>"; | |
446 | } | |
447 | ||
448 | os << "</tr></table>"; | |
449 | } | |
450 | ||
451 | // Within a macro piece. Write out each event. | |
452 | ProcessMacroPiece(os, *MP, 0); | |
453 | } | |
454 | else { | |
455 | os << html::EscapeText(P.getString()); | |
456 | ||
457 | if (max > 1) { | |
458 | os << "</td>"; | |
459 | if (num < max) { | |
460 | os << "<td><div class=\"PathNav\"><a href=\"#"; | |
461 | if (num == max - 1) | |
462 | os << "EndPath"; | |
463 | else | |
464 | os << "Path" << (num + 1); | |
465 | os << "\" title=\"Next event (" | |
466 | << (num + 1) | |
467 | << ")\">→</a></div></td>"; | |
468 | } | |
469 | ||
470 | os << "</tr></table>"; | |
471 | } | |
472 | } | |
473 | ||
474 | os << "</div></td></tr>"; | |
475 | ||
476 | // Insert the new html. | |
477 | unsigned DisplayPos = LineEnd - FileStart; | |
478 | SourceLocation Loc = | |
479 | SM.getLocForStartOfFile(LPosInfo.first).getLocWithOffset(DisplayPos); | |
480 | ||
481 | R.InsertTextBefore(Loc, os.str()); | |
482 | ||
483 | // Now highlight the ranges. | |
484 | ArrayRef<SourceRange> Ranges = P.getRanges(); | |
485 | for (ArrayRef<SourceRange>::iterator I = Ranges.begin(), | |
486 | E = Ranges.end(); I != E; ++I) { | |
487 | HighlightRange(R, LPosInfo.first, *I); | |
488 | } | |
489 | } | |
490 | ||
491 | static void EmitAlphaCounter(raw_ostream &os, unsigned n) { | |
492 | unsigned x = n % ('z' - 'a'); | |
493 | n /= 'z' - 'a'; | |
494 | ||
495 | if (n > 0) | |
496 | EmitAlphaCounter(os, n); | |
497 | ||
498 | os << char('a' + x); | |
499 | } | |
500 | ||
501 | unsigned HTMLDiagnostics::ProcessMacroPiece(raw_ostream &os, | |
502 | const PathDiagnosticMacroPiece& P, | |
503 | unsigned num) { | |
504 | ||
505 | for (PathPieces::const_iterator I = P.subPieces.begin(), E=P.subPieces.end(); | |
506 | I!=E; ++I) { | |
507 | ||
508 | if (const PathDiagnosticMacroPiece *MP = | |
509 | dyn_cast<PathDiagnosticMacroPiece>(*I)) { | |
510 | num = ProcessMacroPiece(os, *MP, num); | |
511 | continue; | |
512 | } | |
513 | ||
514 | if (PathDiagnosticEventPiece *EP = dyn_cast<PathDiagnosticEventPiece>(*I)) { | |
515 | os << "<div class=\"msg msgEvent\" style=\"width:94%; " | |
516 | "margin-left:5px\">" | |
517 | "<table class=\"msgT\"><tr>" | |
518 | "<td valign=\"top\"><div class=\"PathIndex PathIndexEvent\">"; | |
519 | EmitAlphaCounter(os, num++); | |
520 | os << "</div></td><td valign=\"top\">" | |
521 | << html::EscapeText(EP->getString()) | |
522 | << "</td></tr></table></div>\n"; | |
523 | } | |
524 | } | |
525 | ||
526 | return num; | |
527 | } | |
528 | ||
529 | void HTMLDiagnostics::HighlightRange(Rewriter& R, FileID BugFileID, | |
530 | SourceRange Range, | |
531 | const char *HighlightStart, | |
532 | const char *HighlightEnd) { | |
533 | SourceManager &SM = R.getSourceMgr(); | |
534 | const LangOptions &LangOpts = R.getLangOpts(); | |
535 | ||
536 | SourceLocation InstantiationStart = SM.getExpansionLoc(Range.getBegin()); | |
537 | unsigned StartLineNo = SM.getExpansionLineNumber(InstantiationStart); | |
538 | ||
539 | SourceLocation InstantiationEnd = SM.getExpansionLoc(Range.getEnd()); | |
540 | unsigned EndLineNo = SM.getExpansionLineNumber(InstantiationEnd); | |
541 | ||
542 | if (EndLineNo < StartLineNo) | |
543 | return; | |
544 | ||
545 | if (SM.getFileID(InstantiationStart) != BugFileID || | |
546 | SM.getFileID(InstantiationEnd) != BugFileID) | |
547 | return; | |
548 | ||
549 | // Compute the column number of the end. | |
550 | unsigned EndColNo = SM.getExpansionColumnNumber(InstantiationEnd); | |
551 | unsigned OldEndColNo = EndColNo; | |
552 | ||
553 | if (EndColNo) { | |
554 | // Add in the length of the token, so that we cover multi-char tokens. | |
555 | EndColNo += Lexer::MeasureTokenLength(Range.getEnd(), SM, LangOpts)-1; | |
556 | } | |
557 | ||
558 | // Highlight the range. Make the span tag the outermost tag for the | |
559 | // selected range. | |
560 | ||
561 | SourceLocation E = | |
562 | InstantiationEnd.getLocWithOffset(EndColNo - OldEndColNo); | |
563 | ||
564 | html::HighlightRange(R, InstantiationStart, E, HighlightStart, HighlightEnd); | |
565 | } |