Skip to content

Commit 50ca517

Browse files
committed
Allow to check for mis-typed attributes and accidental AST changes
Setting the `QUARTO_JOG_CHECK` environment variable will run checks to identify element attributes that have the wrong type, and will also find filters that modify the input object, but don't return it. Both of these can cause issues with jog.
1 parent 2822eba commit 50ca517

File tree

6 files changed

+526
-2
lines changed

6 files changed

+526
-2
lines changed

src/resources/filters/ast/customnodes.lua

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,51 @@ function is_regular_node(node, name)
4141
return node
4242
end
4343

44-
function run_emulated_filter(doc, filter, traverser)
44+
--- Checks if a filter follows the "nondestructive" property.
45+
-- The nondestructive property is fulfilled if filter functions returns
46+
-- an explicit object, or if it returns `nil` while leaving the passed
47+
-- in object unmodified.
48+
--
49+
-- An error is raised if the property is violated.
50+
--
51+
-- Only filters with this property can use jog safely, without
52+
-- unintended consequences.
53+
local function check_nondestructive_property (filter, filtername)
54+
for name, fn in pairs(filter or {}) do
55+
if type(fn) == 'function' then
56+
local copy = function (x)
57+
local tp = type(x)
58+
return tp ~= 'table' and x:clone() or
59+
(pandoc.utils.type(x) == 'Meta' and pandoc.Meta(x) or tcopy(x, {}))
60+
end
61+
filter[name] = function (obj, context)
62+
local orig = copy(obj)
63+
local result, descend = fn(obj, context)
64+
if result == nil then
65+
if type(obj) ~= 'table' and not tequals(obj, orig) then
66+
warn(
67+
"\nFunction '" .. name ..
68+
"' in filter '" .. (filtername or '<unknown>') ..
69+
"' returned `nil`, but modified the input."
70+
)
71+
end
72+
elseif result.t == obj.t and not rawequal(result, obj) then
73+
warn(
74+
"\nFunction '" .. name ..
75+
"' in filter '" .. (filtername or '<unknown>') ..
76+
"' returned a new object instead of passing the original one through."
77+
)
78+
end
79+
return result, descend
80+
end
81+
end
82+
end
83+
return filter
84+
end
85+
86+
87+
function run_emulated_filter(doc, filter, traverser, name)
88+
name = name or '<unnamed>'
4589
if doc == nil then
4690
return nil
4791
end
@@ -79,6 +123,11 @@ function run_emulated_filter(doc, filter, traverser)
79123
_quarto.traverser = _quarto.utils.walk
80124
elseif traverser == 'jog' then
81125
_quarto.traverser = _quarto.modules.jog
126+
elseif traverser == 'checked-jog' then
127+
_quarto.traverser = function(obj, filter_functions)
128+
check_nondestructive_property(filter_functions, name)
129+
return old_traverse(obj, filter_functions)
130+
end
82131
elseif type(traverser) == 'function' then
83132
_quarto.traverser = traverser
84133
else

src/resources/filters/ast/runemulation.lua

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ end
4949

5050
local function run_emulated_filter_chain(doc, filters, afterFilterPass, profiling)
5151
init_trace(doc)
52+
local compare_jog_and_walk = os.getenv 'QUARTO_JOG_CHECK'
5253
for i, v in ipairs(filters) do
5354
local function callback()
5455
if v.flags then
@@ -79,7 +80,17 @@ local function run_emulated_filter_chain(doc, filters, afterFilterPass, profilin
7980
print(pandoc.write(doc, "native"))
8081
else
8182
_quarto.ast._current_doc = doc
82-
doc = run_emulated_filter(doc, v.filter, v.traverser)
83+
84+
if compare_jog_and_walk and not v.force_pandoc_walk then
85+
v.traverser = 'checked-jog'
86+
end
87+
doc = run_emulated_filter(doc, v.filter, v.traverser, v.name)
88+
89+
if compare_jog_and_walk and not v.force_pandoc_walk then
90+
-- Types of meta values are only checked on assignment.
91+
doc.meta = doc.meta
92+
end
93+
8394
ensure_vault(doc)
8495

8596
add_trace(doc, v.name)

src/resources/filters/common/table.lua

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,53 @@ function spairs(t, order)
101101
end
102102
end
103103
end
104+
105+
--- Checks if two tables are equal
106+
function tequals(o1, o2)
107+
if o1 == o2 then
108+
return true
109+
end
110+
local o1type = type(o1)
111+
local o2type = type(o2)
112+
if o1type ~= o2type or o1type ~= 'table' then
113+
return false
114+
end
115+
116+
local keys = {}
117+
118+
for key1, value1 in pairs(o1) do
119+
local value2 = o2[key1]
120+
if value2 == nil or tequals(value1, value2) == false then
121+
return false
122+
end
123+
keys[key1] = true
124+
end
125+
126+
for key2 in pairs(o2) do
127+
if not keys[key2] then return false end
128+
end
129+
return true
130+
end
131+
132+
--- Create a deep copy of a table.
133+
function tcopy (tbl, seen)
134+
local tp = type(tbl)
135+
if tp == 'table' then
136+
if seen[tbl] then
137+
return seen[tbl]
138+
end
139+
local copy = {}
140+
-- Iterate 'raw' pairs, i.e., without using metamethods
141+
for key, value in next, tbl, nil do
142+
copy[tcopy(key, seen)] = tcopy(value)
143+
end
144+
copy = setmetatable(copy, getmetatable(tbl))
145+
seen[tbl] = copy
146+
return copy
147+
elseif tp == 'userdata' then
148+
return tbl:clone()
149+
else -- number, string, boolean, etc
150+
return tbl
151+
end
152+
end
153+

src/resources/filters/main.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ import("./quarto-init/metainit.lua")
197197

198198
-- [/import]
199199

200+
if os.getenv 'QUARTO_JOG_CHECK' then
201+
_quarto.modules.attribcheck.enable_attribute_checks()
202+
end
203+
200204
initCrossrefIndex()
201205

202206
initShortcodeHandlers()

0 commit comments

Comments
 (0)