Skip to content

Commit 846cc65

Browse files
committed
Allow checks for inadvertent AST modifications
This check is enabled when the QUARTO_JOG_CHECK environment variable is set.
1 parent 525dd97 commit 846cc65

File tree

1 file changed

+91
-10
lines changed

1 file changed

+91
-10
lines changed

src/resources/filters/ast/runemulation.lua

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,95 @@ local function remove_vault(doc)
4747
end
4848
end
4949

50+
--- Create a deep copy of a table.
51+
local function copy_table (tbl, depth)
52+
if type(tbl) == 'table' then
53+
if tbl.clone then
54+
return tbl:clone()
55+
end
56+
57+
local copy = {}
58+
-- Iterate 'raw' pairs, i.e., without using metamethods
59+
for key, value in next, tbl, nil do
60+
if depth == 'shallow' then
61+
copy[key] = value
62+
else
63+
copy[copy_table(key)] = copy_table(value)
64+
end
65+
end
66+
return setmetatable(copy, getmetatable(tbl))
67+
elseif type(tbl) == 'userdata' then
68+
return tbl:clone()
69+
else -- number, string, boolean, etc
70+
return tbl
71+
end
72+
end
73+
74+
--- Checks if two tables are equal
75+
function equals(o1, o2)
76+
if o1 == o2 then
77+
return true
78+
end
79+
local o1type = type(o1)
80+
local o2type = type(o2)
81+
if o1type ~= o2type or o1type ~= 'table' then
82+
return false
83+
end
84+
85+
local keys = {}
86+
87+
for key1, value1 in pairs(o1) do
88+
local value2 = o2[key1]
89+
if value2 == nil or equals(value1, value2) == false then
90+
return false
91+
end
92+
keys[key1] = true
93+
end
94+
95+
for key2, _ in pairs(o2) do
96+
if not keys[key2] then return false end
97+
end
98+
return true
99+
end
100+
101+
--- Checks if a filter follows the "nondestructive" property.
102+
-- The nondestructive property is fulfilled if filter functions returns
103+
-- an explicit object, or if it returns `nil` while leaving the passed
104+
-- in object unmodified.
105+
--
106+
-- An error is raised if the property is violated.
107+
--
108+
-- Only filters with this property can use jog safely, without
109+
-- unintended consequences.
110+
local function check_nondestructive_property (namedfilter)
111+
for name, fn in pairs(namedfilter.filter) do
112+
if type(fn) == 'function' then
113+
local copy = function (x)
114+
return type(x) == 'table' and copy_table(x) or x:clone()
115+
end
116+
namedfilter.filter[name] = function (obj, context)
117+
local orig = copy(obj)
118+
local result, descend = fn(obj, context)
119+
if result == nil then
120+
if not equals(obj, orig) then
121+
warn(
122+
"\nFunction '" .. name .. "' in filter '" .. namedfilter.name ..
123+
"' returned `nil`, but modified the input."
124+
)
125+
end
126+
-- elseif result.t == obj.t and not rawequal(result, obj) then
127+
-- warn(
128+
-- "\nFunction '" .. name .. "' in filter '" .. namedfilter.name ..
129+
-- "' returned a new object instead of passing the original one through."
130+
-- )
131+
end
132+
return result, descend
133+
end
134+
end
135+
end
136+
return namedfilter
137+
end
138+
50139
local function run_emulated_filter_chain(doc, filters, afterFilterPass, profiling)
51140
init_trace(doc)
52141
local compare_jog_and_walk = os.getenv 'QUARTO_JOG_CHECK'
@@ -82,17 +171,9 @@ local function run_emulated_filter_chain(doc, filters, afterFilterPass, profilin
82171
_quarto.ast._current_doc = doc
83172

84173
if compare_jog_and_walk and not v.force_pandoc_walk then
85-
local expected = run_emulated_filter(doc:clone(), v.filter, true)
86-
doc = run_emulated_filter(doc:clone(), v.filter, false)
87-
if doc == expected then
88-
io.stderr:write("[ OK ] " .. v.name .. '\n')
89-
else
90-
io.stderr:write("[FAIL] " .. v.name .. '\n')
91-
doc = expected
92-
end
93-
else
94-
doc = run_emulated_filter(doc, v.filter, v.force_pandoc_walk)
174+
v = check_nondestructive_property(v)
95175
end
176+
doc = run_emulated_filter(doc, v.filter, v.force_pandoc_walk)
96177

97178
ensure_vault(doc)
98179

0 commit comments

Comments
 (0)