1 /**
2    Deals with expanding 3include directives inline.
3  */
4 module dpp.expansion;
5 
6 import dpp.from;
7 
8 version(unittest) {
9     import unit_threaded: shouldEqual;
10 }
11 
12 
13 /**
14    If an #include directive, expand in place (add to context lines)
15    otherwise do nothing (add the line to the context)
16  */
17 void maybeExpand(in string line, ref from!"dpp.runtime.context".Context context)
18     @safe
19 {
20     const headerName = getHeaderName(line);
21 
22     if(headerName == "")
23         context.writeln(line);
24     else
25         expand(toFileName(context.options.includePaths, headerName),  context);
26 }
27 
28 
29 @("translate no include")
30 @safe unittest {
31     import dpp.runtime.context: Context;
32     {
33         Context context;
34         maybeExpand("foo", context);
35         context.translation.shouldEqual("foo");
36     }
37     {
38         Context context;
39         maybeExpand("bar", context);
40         context.translation.shouldEqual("bar");
41     }
42 }
43 
44 private string getHeaderName(string line) @safe pure {
45     import std.algorithm: startsWith, countUntil;
46     import std.range: dropBack;
47     import std.array: popFront;
48     import std..string: stripLeft;
49 
50     line = line.stripLeft;
51     if(!line.startsWith(`#include `)) return "";
52 
53     const openingQuote = line.countUntil!(a => a == '"' || a == '<');
54     const closingQuote = line[openingQuote + 1 .. $].countUntil!(a => a == '"' || a == '>') + openingQuote + 1;
55     return line[openingQuote + 1 .. closingQuote];
56 }
57 
58 @("getHeaderFileName")
59 @safe pure unittest {
60     getHeaderName(`#include "foo.h"`).shouldEqual(`foo.h`);
61     getHeaderName(`#include "bar.h"`).shouldEqual(`bar.h`);
62     getHeaderName(`#include "foo.h" // comment`).shouldEqual(`foo.h`);
63     getHeaderName(`#include <foo.h>`).shouldEqual(`foo.h`);
64     getHeaderName(`    #include "foo.h"`).shouldEqual(`foo.h`);
65 }
66 
67 // transforms a header name, e.g. stdio.h
68 // into a full file path, e.g. /usr/include/stdio.h
69 private string toFileName(in string[] includePaths, in string headerName) @safe {
70 
71     import std.algorithm: map, filter;
72     import std.path: buildPath, absolutePath;
73     import std.file: exists;
74     import std.conv: text;
75     import std.exception: enforce;
76 
77     if(headerName.exists) return headerName;
78 
79     auto filePaths = includePaths
80         .map!(a => buildPath(a, headerName).absolutePath)
81         .filter!exists;
82 
83     enforce(!filePaths.empty, text("d++ cannot find file path for header '", headerName, "'"));
84 
85     return filePaths.front;
86 }
87 
88 
89 void expand(in string headerFileName,
90             ref from!"dpp.runtime.context".Context context,
91             in string file = __FILE__,
92             in size_t line = __LINE__)
93     @safe
94 {
95     import dpp.cursor.translation: translateTopLevelCursor;
96     import clang: parse, TranslationUnitFlags, Cursor;
97     import std.array: join, array;
98     import std.algorithm: sort, filter, map, chunkBy, any;
99 
100     auto translationUnit = parse(headerFileName,
101                                  context.options.includePaths.map!(a => "-I" ~ a).array,
102                                  TranslationUnitFlags.DetailedPreprocessingRecord);
103 
104     // In C there can be several declarations and one definition of a type.
105     // In D we can have only ever one of either. There might be multiple
106     // cursors in the translation unit that all refer to the same canonical type.
107     // Unfortunately, the canonical type is orthogonal to which cursor is the actual
108     // definition, so we prefer to find the definition if it exists, and if not, we
109     // take the canonical declaration so as to not repeat ourselves in D.
110     static Cursor trueCursor(R)(R cursors) {
111 
112         if(cursors.save.any!(a => a.isDefinition))
113             return cursors.filter!(a => a.isDefinition).front;
114 
115         if(cursors.save.any!(a => a.isCanonical))
116             return cursors.filter!(a => a.isCanonical).front;
117 
118         assert(!cursors.empty);
119         return cursors.front;
120     }
121 
122     static bool goodCursor(in Cursor cursor) {
123         return cursor.isCanonical || cursor.isDefinition;
124     }
125 
126     auto cursors = () @trusted {
127         return translationUnit
128         .cursor
129         .children
130         // sort them by canonical cursor
131         .sort!((a, b) => a.canonical.sourceRange.start <
132                          b.canonical.sourceRange.start)
133         // each chunk is a range of cursors representing the same canonical entity
134         .chunkBy!((a, b) => a.canonical == b.canonical)
135         // for each chunk, extract the one cursor we want
136         .map!trueCursor
137         // array is needed for sort
138         .array
139         // libclang gives us macros first, so we sort by line here
140         // (we also just messed up the order above as well)
141         .sort!((a, b) => a.sourceRange.start.line < b.sourceRange.start.line)
142         ;
143     }();
144 
145     const extern_ = isCppHeader(headerFileName) ? "extern(C++)" : "extern(C)";
146     context.writeln([extern_, "{"]);
147 
148     foreach(cursor; cursors) {
149 
150         if(context.hasSeen(cursor)) continue;
151         context.rememberCursor(cursor);
152 
153         const indentation = context.indentation;
154         const lines = translateTopLevelCursor(cursor, context, file, line);
155         if(lines.length) context.writeln(lines);
156         context.setIndentation(indentation);
157     }
158 
159     context.writeln(["}", ""]);
160 }
161 
162 private bool isCppHeader(in string headerFileName) @safe pure {
163     import std.path: extension;
164     return headerFileName.extension != ".h";
165 }