File: | tools/clang/lib/StaticAnalyzer/Core/PlistDiagnostics.cpp |
Warning: | line 393, column 21 Called C++ object pointer is null |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
1 | //===--- PlistDiagnostics.cpp - Plist 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 PlistDiagnostics object. | |||
11 | // | |||
12 | //===----------------------------------------------------------------------===// | |||
13 | ||||
14 | #include "clang/Basic/FileManager.h" | |||
15 | #include "clang/Basic/PlistSupport.h" | |||
16 | #include "clang/Basic/SourceManager.h" | |||
17 | #include "clang/Basic/Version.h" | |||
18 | #include "clang/Lex/Preprocessor.h" | |||
19 | #include "clang/Rewrite/Core/HTMLRewrite.h" | |||
20 | #include "clang/StaticAnalyzer/Core/AnalyzerOptions.h" | |||
21 | #include "clang/StaticAnalyzer/Core/BugReporter/PathDiagnostic.h" | |||
22 | #include "clang/StaticAnalyzer/Core/IssueHash.h" | |||
23 | #include "clang/StaticAnalyzer/Core/PathDiagnosticConsumers.h" | |||
24 | #include "llvm/ADT/Statistic.h" | |||
25 | #include "llvm/ADT/SmallVector.h" | |||
26 | #include "llvm/Support/Casting.h" | |||
27 | using namespace clang; | |||
28 | using namespace ento; | |||
29 | using namespace markup; | |||
30 | ||||
31 | namespace { | |||
32 | class PlistDiagnostics : public PathDiagnosticConsumer { | |||
33 | const std::string OutputFile; | |||
34 | const LangOptions &LangOpts; | |||
35 | const bool SupportsCrossFileDiagnostics; | |||
36 | const bool SerializeStatistics; | |||
37 | public: | |||
38 | PlistDiagnostics(AnalyzerOptions &AnalyzerOpts, | |||
39 | const std::string& prefix, | |||
40 | const LangOptions &LangOpts, | |||
41 | bool supportsMultipleFiles); | |||
42 | ||||
43 | ~PlistDiagnostics() override {} | |||
44 | ||||
45 | void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags, | |||
46 | FilesMade *filesMade) override; | |||
47 | ||||
48 | StringRef getName() const override { | |||
49 | return "PlistDiagnostics"; | |||
50 | } | |||
51 | ||||
52 | PathGenerationScheme getGenerationScheme() const override { | |||
53 | return Extensive; | |||
54 | } | |||
55 | bool supportsLogicalOpControlFlow() const override { return true; } | |||
56 | bool supportsCrossFileDiagnostics() const override { | |||
57 | return SupportsCrossFileDiagnostics; | |||
58 | } | |||
59 | }; | |||
60 | } // end anonymous namespace | |||
61 | ||||
62 | PlistDiagnostics::PlistDiagnostics(AnalyzerOptions &AnalyzerOpts, | |||
63 | const std::string& output, | |||
64 | const LangOptions &LO, | |||
65 | bool supportsMultipleFiles) | |||
66 | : OutputFile(output), | |||
67 | LangOpts(LO), | |||
68 | SupportsCrossFileDiagnostics(supportsMultipleFiles), | |||
69 | SerializeStatistics(AnalyzerOpts.shouldSerializeStats()) {} | |||
70 | ||||
71 | void ento::createPlistDiagnosticConsumer(AnalyzerOptions &AnalyzerOpts, | |||
72 | PathDiagnosticConsumers &C, | |||
73 | const std::string& s, | |||
74 | const Preprocessor &PP) { | |||
75 | C.push_back(new PlistDiagnostics(AnalyzerOpts, s, | |||
76 | PP.getLangOpts(), false)); | |||
77 | } | |||
78 | ||||
79 | void ento::createPlistMultiFileDiagnosticConsumer(AnalyzerOptions &AnalyzerOpts, | |||
80 | PathDiagnosticConsumers &C, | |||
81 | const std::string &s, | |||
82 | const Preprocessor &PP) { | |||
83 | C.push_back(new PlistDiagnostics(AnalyzerOpts, s, | |||
84 | PP.getLangOpts(), true)); | |||
85 | } | |||
86 | ||||
87 | static void ReportControlFlow(raw_ostream &o, | |||
88 | const PathDiagnosticControlFlowPiece& P, | |||
89 | const FIDMap& FM, | |||
90 | const SourceManager &SM, | |||
91 | const LangOptions &LangOpts, | |||
92 | unsigned indent) { | |||
93 | ||||
94 | Indent(o, indent) << "<dict>\n"; | |||
95 | ++indent; | |||
96 | ||||
97 | Indent(o, indent) << "<key>kind</key><string>control</string>\n"; | |||
98 | ||||
99 | // Emit edges. | |||
100 | Indent(o, indent) << "<key>edges</key>\n"; | |||
101 | ++indent; | |||
102 | Indent(o, indent) << "<array>\n"; | |||
103 | ++indent; | |||
104 | for (PathDiagnosticControlFlowPiece::const_iterator I=P.begin(), E=P.end(); | |||
105 | I!=E; ++I) { | |||
106 | Indent(o, indent) << "<dict>\n"; | |||
107 | ++indent; | |||
108 | ||||
109 | // Make the ranges of the start and end point self-consistent with adjacent edges | |||
110 | // by forcing to use only the beginning of the range. This simplifies the layout | |||
111 | // logic for clients. | |||
112 | Indent(o, indent) << "<key>start</key>\n"; | |||
113 | SourceRange StartEdge( | |||
114 | SM.getExpansionLoc(I->getStart().asRange().getBegin())); | |||
115 | EmitRange(o, SM, Lexer::getAsCharRange(StartEdge, SM, LangOpts), FM, | |||
116 | indent + 1); | |||
117 | ||||
118 | Indent(o, indent) << "<key>end</key>\n"; | |||
119 | SourceRange EndEdge(SM.getExpansionLoc(I->getEnd().asRange().getBegin())); | |||
120 | EmitRange(o, SM, Lexer::getAsCharRange(EndEdge, SM, LangOpts), FM, | |||
121 | indent + 1); | |||
122 | ||||
123 | --indent; | |||
124 | Indent(o, indent) << "</dict>\n"; | |||
125 | } | |||
126 | --indent; | |||
127 | Indent(o, indent) << "</array>\n"; | |||
128 | --indent; | |||
129 | ||||
130 | // Output any helper text. | |||
131 | const auto &s = P.getString(); | |||
132 | if (!s.empty()) { | |||
133 | Indent(o, indent) << "<key>alternate</key>"; | |||
134 | EmitString(o, s) << '\n'; | |||
135 | } | |||
136 | ||||
137 | --indent; | |||
138 | Indent(o, indent) << "</dict>\n"; | |||
139 | } | |||
140 | ||||
141 | static void ReportEvent(raw_ostream &o, const PathDiagnosticPiece& P, | |||
142 | const FIDMap& FM, | |||
143 | const SourceManager &SM, | |||
144 | const LangOptions &LangOpts, | |||
145 | unsigned indent, | |||
146 | unsigned depth, | |||
147 | bool isKeyEvent = false) { | |||
148 | ||||
149 | Indent(o, indent) << "<dict>\n"; | |||
150 | ++indent; | |||
151 | ||||
152 | Indent(o, indent) << "<key>kind</key><string>event</string>\n"; | |||
153 | ||||
154 | if (isKeyEvent) { | |||
155 | Indent(o, indent) << "<key>key_event</key><true/>\n"; | |||
156 | } | |||
157 | ||||
158 | // Output the location. | |||
159 | FullSourceLoc L = P.getLocation().asLocation(); | |||
160 | ||||
161 | Indent(o, indent) << "<key>location</key>\n"; | |||
162 | EmitLocation(o, SM, L, FM, indent); | |||
163 | ||||
164 | // Output the ranges (if any). | |||
165 | ArrayRef<SourceRange> Ranges = P.getRanges(); | |||
166 | ||||
167 | if (!Ranges.empty()) { | |||
168 | Indent(o, indent) << "<key>ranges</key>\n"; | |||
169 | Indent(o, indent) << "<array>\n"; | |||
170 | ++indent; | |||
171 | for (auto &R : Ranges) | |||
172 | EmitRange(o, SM, | |||
173 | Lexer::getAsCharRange(SM.getExpansionRange(R), SM, LangOpts), | |||
174 | FM, indent + 1); | |||
175 | --indent; | |||
176 | Indent(o, indent) << "</array>\n"; | |||
177 | } | |||
178 | ||||
179 | // Output the call depth. | |||
180 | Indent(o, indent) << "<key>depth</key>"; | |||
181 | EmitInteger(o, depth) << '\n'; | |||
182 | ||||
183 | // Output the text. | |||
184 | assert(!P.getString().empty())(static_cast <bool> (!P.getString().empty()) ? void (0) : __assert_fail ("!P.getString().empty()", "/build/llvm-toolchain-snapshot-7~svn329677/tools/clang/lib/StaticAnalyzer/Core/PlistDiagnostics.cpp" , 184, __extension__ __PRETTY_FUNCTION__)); | |||
185 | Indent(o, indent) << "<key>extended_message</key>\n"; | |||
186 | Indent(o, indent); | |||
187 | EmitString(o, P.getString()) << '\n'; | |||
188 | ||||
189 | // Output the short text. | |||
190 | // FIXME: Really use a short string. | |||
191 | Indent(o, indent) << "<key>message</key>\n"; | |||
192 | Indent(o, indent); | |||
193 | EmitString(o, P.getString()) << '\n'; | |||
194 | ||||
195 | // Finish up. | |||
196 | --indent; | |||
197 | Indent(o, indent); o << "</dict>\n"; | |||
198 | } | |||
199 | ||||
200 | static void ReportPiece(raw_ostream &o, | |||
201 | const PathDiagnosticPiece &P, | |||
202 | const FIDMap& FM, const SourceManager &SM, | |||
203 | const LangOptions &LangOpts, | |||
204 | unsigned indent, | |||
205 | unsigned depth, | |||
206 | bool includeControlFlow, | |||
207 | bool isKeyEvent = false); | |||
208 | ||||
209 | static void ReportCall(raw_ostream &o, | |||
210 | const PathDiagnosticCallPiece &P, | |||
211 | const FIDMap& FM, const SourceManager &SM, | |||
212 | const LangOptions &LangOpts, | |||
213 | unsigned indent, | |||
214 | unsigned depth) { | |||
215 | ||||
216 | if (auto callEnter = P.getCallEnterEvent()) | |||
217 | ReportPiece(o, *callEnter, FM, SM, LangOpts, indent, depth, true, | |||
218 | P.isLastInMainSourceFile()); | |||
219 | ||||
220 | ||||
221 | ++depth; | |||
222 | ||||
223 | if (auto callEnterWithinCaller = P.getCallEnterWithinCallerEvent()) | |||
224 | ReportPiece(o, *callEnterWithinCaller, FM, SM, LangOpts, | |||
225 | indent, depth, true); | |||
226 | ||||
227 | for (PathPieces::const_iterator I = P.path.begin(), E = P.path.end();I!=E;++I) | |||
228 | ReportPiece(o, **I, FM, SM, LangOpts, indent, depth, true); | |||
229 | ||||
230 | --depth; | |||
231 | ||||
232 | if (auto callExit = P.getCallExitEvent()) | |||
233 | ReportPiece(o, *callExit, FM, SM, LangOpts, indent, depth, true); | |||
234 | } | |||
235 | ||||
236 | static void ReportMacro(raw_ostream &o, | |||
237 | const PathDiagnosticMacroPiece& P, | |||
238 | const FIDMap& FM, const SourceManager &SM, | |||
239 | const LangOptions &LangOpts, | |||
240 | unsigned indent, | |||
241 | unsigned depth) { | |||
242 | ||||
243 | for (PathPieces::const_iterator I = P.subPieces.begin(), E=P.subPieces.end(); | |||
244 | I!=E; ++I) { | |||
245 | ReportPiece(o, **I, FM, SM, LangOpts, indent, depth, false); | |||
246 | } | |||
247 | } | |||
248 | ||||
249 | static void ReportDiag(raw_ostream &o, const PathDiagnosticPiece& P, | |||
250 | const FIDMap& FM, const SourceManager &SM, | |||
251 | const LangOptions &LangOpts) { | |||
252 | ReportPiece(o, P, FM, SM, LangOpts, 4, 0, true); | |||
253 | } | |||
254 | ||||
255 | static void ReportPiece(raw_ostream &o, | |||
256 | const PathDiagnosticPiece &P, | |||
257 | const FIDMap& FM, const SourceManager &SM, | |||
258 | const LangOptions &LangOpts, | |||
259 | unsigned indent, | |||
260 | unsigned depth, | |||
261 | bool includeControlFlow, | |||
262 | bool isKeyEvent) { | |||
263 | switch (P.getKind()) { | |||
264 | case PathDiagnosticPiece::ControlFlow: | |||
265 | if (includeControlFlow) | |||
266 | ReportControlFlow(o, cast<PathDiagnosticControlFlowPiece>(P), FM, SM, | |||
267 | LangOpts, indent); | |||
268 | break; | |||
269 | case PathDiagnosticPiece::Call: | |||
270 | ReportCall(o, cast<PathDiagnosticCallPiece>(P), FM, SM, LangOpts, | |||
271 | indent, depth); | |||
272 | break; | |||
273 | case PathDiagnosticPiece::Event: | |||
274 | ReportEvent(o, cast<PathDiagnosticSpotPiece>(P), FM, SM, LangOpts, | |||
275 | indent, depth, isKeyEvent); | |||
276 | break; | |||
277 | case PathDiagnosticPiece::Macro: | |||
278 | ReportMacro(o, cast<PathDiagnosticMacroPiece>(P), FM, SM, LangOpts, | |||
279 | indent, depth); | |||
280 | break; | |||
281 | case PathDiagnosticPiece::Note: | |||
282 | // FIXME: Extend the plist format to support those. | |||
283 | break; | |||
284 | } | |||
285 | } | |||
286 | ||||
287 | void PlistDiagnostics::FlushDiagnosticsImpl( | |||
288 | std::vector<const PathDiagnostic *> &Diags, | |||
289 | FilesMade *filesMade) { | |||
290 | // Build up a set of FIDs that we use by scanning the locations and | |||
291 | // ranges of the diagnostics. | |||
292 | FIDMap FM; | |||
293 | SmallVector<FileID, 10> Fids; | |||
294 | const SourceManager* SM = nullptr; | |||
| ||||
295 | ||||
296 | if (!Diags.empty()) | |||
297 | SM = &Diags.front()->path.front()->getLocation().getManager(); | |||
298 | ||||
299 | auto AddPieceFID = [&FM, &Fids, SM](const PathDiagnosticPiece &Piece) { | |||
300 | AddFID(FM, Fids, *SM, Piece.getLocation().asLocation()); | |||
301 | ArrayRef<SourceRange> Ranges = Piece.getRanges(); | |||
302 | for (const SourceRange &Range : Ranges) { | |||
303 | AddFID(FM, Fids, *SM, Range.getBegin()); | |||
304 | AddFID(FM, Fids, *SM, Range.getEnd()); | |||
305 | } | |||
306 | }; | |||
307 | ||||
308 | for (const PathDiagnostic *D : Diags) { | |||
309 | ||||
310 | SmallVector<const PathPieces *, 5> WorkList; | |||
311 | WorkList.push_back(&D->path); | |||
312 | ||||
313 | while (!WorkList.empty()) { | |||
314 | const PathPieces &Path = *WorkList.pop_back_val(); | |||
315 | ||||
316 | for (const auto &Iter : Path) { | |||
317 | const PathDiagnosticPiece &Piece = *Iter; | |||
318 | AddPieceFID(Piece); | |||
319 | ||||
320 | if (const PathDiagnosticCallPiece *Call = | |||
321 | dyn_cast<PathDiagnosticCallPiece>(&Piece)) { | |||
322 | if (auto CallEnterWithin = Call->getCallEnterWithinCallerEvent()) | |||
323 | AddPieceFID(*CallEnterWithin); | |||
324 | ||||
325 | if (auto CallEnterEvent = Call->getCallEnterEvent()) | |||
326 | AddPieceFID(*CallEnterEvent); | |||
327 | ||||
328 | WorkList.push_back(&Call->path); | |||
329 | } else if (const PathDiagnosticMacroPiece *Macro = | |||
330 | dyn_cast<PathDiagnosticMacroPiece>(&Piece)) { | |||
331 | WorkList.push_back(&Macro->subPieces); | |||
332 | } | |||
333 | } | |||
334 | } | |||
335 | } | |||
336 | ||||
337 | // Open the file. | |||
338 | std::error_code EC; | |||
339 | llvm::raw_fd_ostream o(OutputFile, EC, llvm::sys::fs::F_Text); | |||
340 | if (EC) { | |||
341 | llvm::errs() << "warning: could not create file: " << EC.message() << '\n'; | |||
342 | return; | |||
343 | } | |||
344 | ||||
345 | EmitPlistHeader(o); | |||
346 | ||||
347 | // Write the root object: a <dict> containing... | |||
348 | // - "clang_version", the string representation of clang version | |||
349 | // - "files", an <array> mapping from FIDs to file names | |||
350 | // - "diagnostics", an <array> containing the path diagnostics | |||
351 | o << "<dict>\n" << | |||
352 | " <key>clang_version</key>\n"; | |||
353 | EmitString(o, getClangFullVersion()) << '\n'; | |||
354 | o << " <key>files</key>\n" | |||
355 | " <array>\n"; | |||
356 | ||||
357 | for (FileID FID : Fids) | |||
358 | EmitString(o << " ", SM->getFileEntryForID(FID)->getName()) << '\n'; | |||
359 | ||||
360 | o << " </array>\n" | |||
361 | " <key>diagnostics</key>\n" | |||
362 | " <array>\n"; | |||
363 | ||||
364 | for (std::vector<const PathDiagnostic*>::iterator DI=Diags.begin(), | |||
365 | DE = Diags.end(); DI!=DE; ++DI) { | |||
366 | ||||
367 | o << " <dict>\n" | |||
368 | " <key>path</key>\n"; | |||
369 | ||||
370 | const PathDiagnostic *D = *DI; | |||
371 | ||||
372 | o << " <array>\n"; | |||
373 | ||||
374 | for (PathPieces::const_iterator I = D->path.begin(), E = D->path.end(); | |||
375 | I != E; ++I) | |||
376 | ReportDiag(o, **I, FM, *SM, LangOpts); | |||
377 | ||||
378 | o << " </array>\n"; | |||
379 | ||||
380 | // Output the bug type and bug category. | |||
381 | o << " <key>description</key>"; | |||
382 | EmitString(o, D->getShortDescription()) << '\n'; | |||
383 | o << " <key>category</key>"; | |||
384 | EmitString(o, D->getCategory()) << '\n'; | |||
385 | o << " <key>type</key>"; | |||
386 | EmitString(o, D->getBugType()) << '\n'; | |||
387 | o << " <key>check_name</key>"; | |||
388 | EmitString(o, D->getCheckName()) << '\n'; | |||
389 | ||||
390 | o << " <!-- This hash is experimental and going to change! -->\n"; | |||
391 | o << " <key>issue_hash_content_of_line_in_context</key>"; | |||
392 | PathDiagnosticLocation UPDLoc = D->getUniqueingLoc(); | |||
393 | FullSourceLoc L(SM->getExpansionLoc(UPDLoc.isValid() | |||
| ||||
394 | ? UPDLoc.asLocation() | |||
395 | : D->getLocation().asLocation()), | |||
396 | *SM); | |||
397 | const Decl *DeclWithIssue = D->getDeclWithIssue(); | |||
398 | EmitString(o, GetIssueHash(*SM, L, D->getCheckName(), D->getBugType(), | |||
399 | DeclWithIssue, LangOpts)) | |||
400 | << '\n'; | |||
401 | ||||
402 | // Output information about the semantic context where | |||
403 | // the issue occurred. | |||
404 | if (const Decl *DeclWithIssue = D->getDeclWithIssue()) { | |||
405 | // FIXME: handle blocks, which have no name. | |||
406 | if (const NamedDecl *ND = dyn_cast<NamedDecl>(DeclWithIssue)) { | |||
407 | StringRef declKind; | |||
408 | switch (ND->getKind()) { | |||
409 | case Decl::CXXRecord: | |||
410 | declKind = "C++ class"; | |||
411 | break; | |||
412 | case Decl::CXXMethod: | |||
413 | declKind = "C++ method"; | |||
414 | break; | |||
415 | case Decl::ObjCMethod: | |||
416 | declKind = "Objective-C method"; | |||
417 | break; | |||
418 | case Decl::Function: | |||
419 | declKind = "function"; | |||
420 | break; | |||
421 | default: | |||
422 | break; | |||
423 | } | |||
424 | if (!declKind.empty()) { | |||
425 | const std::string &declName = ND->getDeclName().getAsString(); | |||
426 | o << " <key>issue_context_kind</key>"; | |||
427 | EmitString(o, declKind) << '\n'; | |||
428 | o << " <key>issue_context</key>"; | |||
429 | EmitString(o, declName) << '\n'; | |||
430 | } | |||
431 | ||||
432 | // Output the bug hash for issue unique-ing. Currently, it's just an | |||
433 | // offset from the beginning of the function. | |||
434 | if (const Stmt *Body = DeclWithIssue->getBody()) { | |||
435 | ||||
436 | // If the bug uniqueing location exists, use it for the hash. | |||
437 | // For example, this ensures that two leaks reported on the same line | |||
438 | // will have different issue_hashes and that the hash will identify | |||
439 | // the leak location even after code is added between the allocation | |||
440 | // site and the end of scope (leak report location). | |||
441 | if (UPDLoc.isValid()) { | |||
442 | FullSourceLoc UFunL(SM->getExpansionLoc( | |||
443 | D->getUniqueingDecl()->getBody()->getLocStart()), *SM); | |||
444 | o << " <key>issue_hash_function_offset</key><string>" | |||
445 | << L.getExpansionLineNumber() - UFunL.getExpansionLineNumber() | |||
446 | << "</string>\n"; | |||
447 | ||||
448 | // Otherwise, use the location on which the bug is reported. | |||
449 | } else { | |||
450 | FullSourceLoc FunL(SM->getExpansionLoc(Body->getLocStart()), *SM); | |||
451 | o << " <key>issue_hash_function_offset</key><string>" | |||
452 | << L.getExpansionLineNumber() - FunL.getExpansionLineNumber() | |||
453 | << "</string>\n"; | |||
454 | } | |||
455 | ||||
456 | } | |||
457 | } | |||
458 | } | |||
459 | ||||
460 | // Output the location of the bug. | |||
461 | o << " <key>location</key>\n"; | |||
462 | EmitLocation(o, *SM, D->getLocation().asLocation(), FM, 2); | |||
463 | ||||
464 | // Output the diagnostic to the sub-diagnostic client, if any. | |||
465 | if (!filesMade->empty()) { | |||
466 | StringRef lastName; | |||
467 | PDFileEntry::ConsumerFiles *files = filesMade->getFiles(*D); | |||
468 | if (files) { | |||
469 | for (PDFileEntry::ConsumerFiles::const_iterator CI = files->begin(), | |||
470 | CE = files->end(); CI != CE; ++CI) { | |||
471 | StringRef newName = CI->first; | |||
472 | if (newName != lastName) { | |||
473 | if (!lastName.empty()) { | |||
474 | o << " </array>\n"; | |||
475 | } | |||
476 | lastName = newName; | |||
477 | o << " <key>" << lastName << "_files</key>\n"; | |||
478 | o << " <array>\n"; | |||
479 | } | |||
480 | o << " <string>" << CI->second << "</string>\n"; | |||
481 | } | |||
482 | o << " </array>\n"; | |||
483 | } | |||
484 | } | |||
485 | ||||
486 | // Close up the entry. | |||
487 | o << " </dict>\n"; | |||
488 | } | |||
489 | ||||
490 | o << " </array>\n"; | |||
491 | ||||
492 | if (llvm::AreStatisticsEnabled() && SerializeStatistics) { | |||
493 | o << " <key>statistics</key>\n"; | |||
494 | std::string stats; | |||
495 | llvm::raw_string_ostream os(stats); | |||
496 | llvm::PrintStatisticsJSON(os); | |||
497 | os.flush(); | |||
498 | EmitString(o, html::EscapeText(stats)) << '\n'; | |||
499 | } | |||
500 | ||||
501 | // Finish. | |||
502 | o << "</dict>\n</plist>"; | |||
503 | } |