1--
2-- criteria.lua
3--
4-- Stores a list of criteria terms with support for negation, conjunction,
5-- and wildcard matches. Provides functions match match these criteria
6-- against various contexts.
7--
8-- Copyright (c) 2012-2015 Jason Perkins and the Premake project
9--
10
11	local p = premake
12
13	p.criteria = criteria  -- criteria namespace is defined in C host
14	local criteria = p.criteria
15
16
17--
18-- These prefixes correspond to the context information built by the oven
19-- during baking. In theory, any field could be used as a filter, but right
20-- now only these are set.
21--
22
23	criteria._validPrefixes = {
24		_action = true,
25		action = true,
26		architecture = true,
27		configurations = true,
28		files = true,
29		kind = true,
30		language = true,
31		_options = true,
32		options = true,
33		platforms = true,
34		system = true,
35		toolset = true,
36		tags = true,
37	}
38
39
40--
41-- Flattens a hierarchy of criteria terms into a single array containing all
42-- of the values as strings in the form of "term:value1 or value2" etc.
43--
44	function criteria.flatten(terms)
45		local result = {}
46
47		local function flatten(terms)
48			for key, value in pairs(terms) do
49				if type(key) == "number" then
50					if type(value) == "table" then
51						flatten(value)
52					elseif value then
53						table.insert(result, value)
54					end
55				elseif type(key) == "string" then
56					local word = key .. ":"
57					if type(value) == "table" then
58						local values = table.flatten(value)
59						word = word .. table.concat(values, " or ")
60					else
61						word = word .. value
62					end
63					table.insert(result, word)
64				else
65					error("Unknown key type in terms.")
66				end
67			end
68		end
69
70		flatten(terms)
71		return result
72	end
73
74---
75-- Create a new criteria object.
76--
77-- @param terms
78--    A list of criteria terms.
79-- @param unprefixed
80--    If true, use the old style, unprefixed filter terms. This will
81--    eventually be phased out in favor of prefixed terms only.
82-- @return
83--    A new criteria object.
84---
85
86	function criteria.new(terms, unprefixed)
87		terms = criteria.flatten(terms)
88
89		-- Preprocess the list of terms for better performance in matches().
90		-- Each term is replaced with a pattern, with an implied AND between
91		-- them. Each pattern contains one or more words, with an implied OR
92		-- between them. A word maybe be flagged as negated, or as a wildcard
93		-- pattern, and may have a field prefix associated with it.
94
95		local patterns = {}
96
97		for i, term in ipairs(terms) do
98			term = term:lower()
99
100			local pattern = {}
101			local prefix = iif(unprefixed, nil, "configurations")
102
103			local words = term:explode(" or ")
104			for _, word in ipairs(words) do
105				word, prefix = criteria._word(word, prefix)
106				if prefix and not criteria._validPrefixes[prefix] then
107					return nil, string.format("Invalid field prefix '%s'", prefix)
108				end
109
110				-- check for field value aliases
111				if prefix then
112					local fld = p.field.get(prefix)
113					if fld and fld.aliases then
114						word[1] = fld.aliases[word[1]] or word[1]
115					end
116				end
117
118				table.insert(pattern, word)
119			end
120
121			table.insert(patterns, pattern)
122		end
123
124		-- The matching logic is written in C now for performance; compile
125		-- this collection of patterns to C data structures to make that
126		-- code easier to read and maintain.
127
128		local crit = {}
129		crit.patterns = patterns
130		crit.data = criteria._compile(patterns)
131		crit.terms = terms
132		return crit
133	end
134
135
136
137	function criteria._word(word, prefix)
138		local wildcard
139		local assertion = true
140
141		-- Trim off all "not" and field prefixes and check for wildcards
142		while (true) do
143			if word:startswith("not ") then
144				assertion = not assertion
145				word = word:sub(5)
146			else
147				local i = word:find(":", 1, true)
148				if prefix and i then
149					prefix = word:sub(1, i - 1)
150					word = word:sub(i + 1)
151				else
152					wildcard = (word:find("*", 1, true) ~= nil)
153					if wildcard then
154						word = path.wildcards(word)
155					end
156					break
157				end
158			end
159		end
160
161		return { word, prefix, assertion, wildcard }, prefix
162	end
163
164
165---
166-- Add a new prefix to the list of allowed values for filters. Note
167-- setting a prefix on its own has no effect on the output; a filter
168-- term must also be set on the corresponding context during baking.
169--
170-- @param prefix
171--    The new prefix to be allowed.
172---
173
174	function criteria.allowPrefix(prefix)
175		criteria._validPrefixes[prefix:lower()] = true
176	end
177
178