-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathcompile_commands_reader.lua
255 lines (204 loc) · 7.71 KB
/
compile_commands_reader.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
local io = require("io")
local math = require("math")
local util = require("util")
local compile_commands_util = require("compile_commands_util")
local check = require("error_util").check
local assert = assert
local ipairs = ipairs
local loadstring = loadstring
local pairs = pairs
local pcall = pcall
local setfenv = setfenv
local type = type
----------
local api = {}
local function tweak_json_string_for_load_as_lua_table(str)
-- replace a lone '[]' with '{}' so that an empty compile_commands.json is handled
str = str:gsub("^%[%]", "{}")
-- replace leading/trailing matching '[ (...) ]' with '{ (...) }'
str = str:gsub("^%[\n", "{\n"):gsub("\n%]\n?$", "\n}")
-- replace any other '[]' (expected: of 'arguments' key, if present)
str = str:gsub(": %[\n", ": {\n"):gsub("%], *\n", "},\n")
-- replace '"<key>": ' by 'key= ' (expected to occur in what would now be a Lua table)
return "return "..str:gsub('\n( *)"([a-z]+)": ', '\n%1%2= ')
end
local PREFIX = "ERROR: Unexpected input: parsed result "
local function validate_compile_commands_table(cmds)
if (type(cmds) ~= "table") then
return PREFIX.."is not a Lua table"
end
local numCmds = #cmds
for k,_ in pairs(cmds) do
if (type(k) ~= "number") then
return PREFIX.."contains non-numeric keys"
end
if (math.floor(k) ~= k) then
return PREFIX.."contains non-integral numeric keys"
end
if (not (k >= 1 and k <= numCmds)) then
return PREFIX.."contains numeric keys inconsistent with #table"
end
end
local hasArgs = (numCmds > 0 and cmds[1].arguments ~= nil)
local hasCommand = (numCmds > 0 and cmds[1].command ~= nil)
if (numCmds > 0 and not hasArgs and not hasCommand) then
return PREFIX.."is non-empty but its first element contains neither key 'arguments' nor 'command'"
end
local key = hasArgs and "arguments" or "command"
local expectedKeyType = hasArgs and "table" or "string"
local expectedMissingKey = hasArgs and "command" or "arguments"
for _, cmd in ipairs(cmds) do
if (type(cmd) ~= "table") then
return PREFIX.."contains missing or non-table elements"
end
if (type(cmd.directory) ~= "string") then
return PREFIX.."contains an element with key 'directory' missing or not of string type"
end
if (type(cmd.file) ~= "string") then
return PREFIX.."contains an element with key 'file' missing or not of string type"
end
if (type(cmd[key]) ~= expectedKeyType) then
return PREFIX.."contains an element with key '"..key..
"' missing or not of "..expectedKeyType.." type"
end
if (cmd[expectedMissingKey] ~= nil) then
return PREFIX.."contains and element with key '"..expectedMissingKey..
"' unexpectedly present"
end
if (hasCommand and cmd.command:match("\\%s")) then
-- We will split the command by whitespace, so escaped whitespace characters
-- would throw us off the track. For now, bail out if we come across that case.
return PREFIX.."contains an element with key 'command' "..
"containing a backslash followed by whitespace (not implemented)"
end
end
return nil, hasCommand
end
-- If the entries have key 'command' (and thus do not have key 'args', since we validated
-- mutual exclusion), add key 'args'. Also make keys 'file' absolute file names by prefixing
-- them with key 'directory' whenever they are not already absolute.
local function tweak_compile_commands_table(cmds, hasCommand)
assert(type(cmds) == "table")
if (hasCommand) then
for _, cmd in ipairs(cmds) do
local argv = util.splitAtWhitespace(cmd.command)
local arguments = {}
for i = 2,#argv do
-- Keep only the arguments, not the invoked compiler executable name.
assert(type(argv[i]) == "string")
arguments[i - 1] = argv[i]
end
cmd.arguments = arguments
cmd.compiler_executable = argv[1]
cmd.command = nil
end
else
for _, cmd in ipairs(cmds) do
local args = cmd.arguments
cmd.compiler_executable = args[1]
for i = 1, #args do
-- Shift elements of 'args' one left.
args[i] = args[i+1]
end
end
end
for _, cmd in ipairs(cmds) do
-- The key 'file' as it appears in the compile_commands.json:
local compiledFileName = cmd.file
-- Absify it:
local absoluteFileName = compile_commands_util.absify(cmd.file, cmd.directory)
assert(type(absoluteFileName) == "string")
cmd.file = absoluteFileName
-- And also absify it appearing in the argument list.
local matchCount = 0
for ai, arg in ipairs(cmd.arguments) do
if (arg == compiledFileName) then
cmd.arguments[ai] = absoluteFileName
matchCount = matchCount + 1
end
end
cmd.arguments, cmd.pchArguments =
compile_commands_util.sanitize_args(cmd.arguments, cmd.directory)
-- NOTE: "== 1" is overly strict. I'm just curious about the situation in the wild.
if (matchCount ~= 1) then
return nil, PREFIX.."contains an entry for which the name of "..
"the compiled file is not found exactly once in the compiler arguments"
end
for ai, arg in ipairs(cmd.arguments) do
if (arg == absoluteFileName) then
cmd.fileNameIdx = ai
break
end
end
assert(cmd.fileNameIdx ~= nil)
end
return cmds
end
local function load_json_as_lua_string(str)
local func, errmsg = loadstring(str, "compile_commands.json as Lua table")
if (func == nil) then
return nil, errmsg
end
-- Completely empty the function's environment as an additional safety measure,
-- then run the chunk protected.
local ok, result = pcall(setfenv(func, {}))
if (not ok) then
assert(type(result) == "string") -- error message
return nil, result
end
local errmsg, hasCommand = validate_compile_commands_table(result)
if (errmsg ~= nil) then
return nil, errmsg
end
return tweak_compile_commands_table(result, hasCommand)
end
-- Parses a compile_commands.json file, returning a Lua table.
-- On failure, returns nil and an error message.
--
-- Supported formats:
--
-- [
-- <entry_0>,
-- <entry_1>,
-- ...
-- ]
--
-- with <entry_i> being either (1)
--
-- {
-- "arguments": [ <string>, <string>, ... ],
-- "directory": <string>,
-- "file": <string>
-- }
--
-- or (2)
--
-- {
-- "command": <string> (compiler executable followed by its arguments, whitespace-separated)
-- "directory": <string>,
-- "file": <string>
-- }
--
-- The returned table always contains entries of the form (1). Backslashes followed by
-- whitespace in the "command" key in form (2) are rejected.
function api.parse_compile_commands(compile_commands_string)
check(type(compile_commands_string) == "string",
"<compile_commands_string> must be a string", 2)
local str = tweak_json_string_for_load_as_lua_table(compile_commands_string)
return load_json_as_lua_string(str)
end
function api.read_compile_commands(filename)
check(type(filename) == "string", "<filename> must be a string", 2)
local f, msg = io.open(filename)
if (f == nil) then
return nil, msg
end
local str, msg = f:read("*a")
f:close()
if (str == nil) then
return nil, msg
end
return api.parse_compile_commands(str)
end
-- Done!
return api