summaryrefslogtreecommitdiff
path: root/xmake/modules/private/check/checkers/api/api_checker.lua
blob: 0625d2cfaf3e00505ffb0a61982bb5e6531f7418 (plain)
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
--!A cross-platform build utility based on Lua
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015-present, Xmake Open Source Community.
--
-- @author      ruki
-- @file        api_checker.lua
--

-- imports
import("core.base.option")
import("core.base.hashset")
import("core.package.package")
import("core.project.project")
import("private.check.checker")
import("private.utils.target", {alias = "target_utils"})

function _get_project_packages()
    local project_packages = _g.project_packages
    if not project_packages then
        project_packages = {}
        for name, _ in pairs(project.required_packages()) do
            local pkg, errors = package.load_from_project(name)
            if pkg then
                table.insert(project_packages, pkg)
            elseif errors then
                raise(errors)
            end
        end
        _g.project_packages = project_packages
    end
    return project_packages
end

-- get the most probable value
function _get_most_probable_value(value, valueset)
    local result
    local mindist
    for v in valueset:keys() do
        local dist = value:levenshtein(v)
        if not mindist or dist < mindist then
            mindist = dist
            result = v
        end
    end
    return result
end

function _do_show(str, opt)
    _g.showed = _g.showed or {}
    local showed = _g.showed
    local infostr
    if str then
        infostr = string.format("%s: %s: %s", opt.sourcetips, opt.level_tips, str)
    else
        infostr = string.format("%s: %s: unknown %s value '%s'", opt.sourcetips, opt.level_tips, opt.apiname, opt.value)
    end
    if opt.probable_value then
        infostr = string.format("%s, it may be '%s'", infostr, opt.probable_value)
    end
    if not showed[infostr] then
        cprint(infostr)
        showed[infostr] = true
        return true
    end
end

-- show result
function _show(apiname, value, instance, opt)
    opt = opt or {}

    -- match level? verbose: note/warning/error, default: warning/error
    local level = opt.level
    if not option.get("verbose") and level == "note" then
        return
    end

    -- get source information
    local sourceinfo = instance:sourceinfo(apiname, value) or {}
    local sourcetips = sourceinfo.file or ""
    if sourceinfo.line then
        sourcetips = sourcetips .. ":" .. (sourceinfo.line or -1)
    end
    if #sourcetips == 0 then
        sourcetips = string.format("%s(%s)", instance:type(), instance:name())
    end

    -- get level tips
    local level_tips = "note"
    if level == "warning" then
        level_tips = "${color.warning}${text.warning}${clear}"
    elseif level == "error" then
        level_tips = "${color.error}${text.error}${clear}"
    end

    -- get probable value
    local probable_value
    if opt.valueset then
        probable_value = _get_most_probable_value(value, opt.valueset)
    end

    if apiname:endswith("s") then
        apiname = apiname:sub(1, #apiname - 1)
    end

    -- do show
    return (opt.show or _do_show)(opt.showstr, {
        apiname = apiname,
        sourcetips = sourcetips,
        level_tips = level_tips,
        value = value,
        probable_value = probable_value})
end

-- report the invalid value on the given instance
function _report(instance, apiname, value, level, opt)
    local reported = _show(apiname, value, instance, {
        show = opt.show,
        showstr = opt.showstr,
        valueset = opt.valueset,
        level = level})
    if reported then
        checker.update_stats(level)
    end
end

-- check instance
function _check_instance(instance, apiname, valueset, level, opt)
    local instance_valueset = valueset
    if type(opt.values) == "function" then
        local instance_values = opt.values(instance)
        if instance_values then
            instance_valueset = hashset.from(instance_values)
        end
    end
    local values = instance:get(apiname)

    -- check the keyvalues api, e.g. set_toolset("cxx", "clang")
    -- the values is a dictionary, e.g. {cxx = "clang"}, so we report it on the key
    -- @see https://github.com/xmake-io/xmake/pull/7597
    if opt.keyvalues then
        if opt.check then
            for key, value in pairs(table.wrap(values)) do
                local ok, errors = opt.check(instance, key, value)
                if not ok then
                    _report(instance, apiname, key, level, {show = opt.show, showstr = errors})
                end
            end
        end
        return
    end

    for _, value in ipairs(values) do
        if opt.check then
            local ok, errors = opt.check(instance, value)
            if not ok then
                _report(instance, apiname, value, level, {show = opt.show, showstr = errors})
            end
        elseif not instance_valueset:has(value) then
            _report(instance, apiname, value, level, {show = opt.show, valueset = instance_valueset})
        end
    end
end

-- check api configuration in instances
function _check_instances(apiname, instance, instances_func, opt)
    local level = opt.level or "warning"
    local valueset
    if opt.values and type(opt.values) ~= "function" then
        valueset = hashset.from(opt.values)
    else
        valueset = hashset.new()
    end
    if instance then
        _check_instance(instance, apiname, valueset, level, opt)
    else
        for _, instance in pairs(instances_func()) do
            _check_instance(instance, apiname, valueset, level, opt)
        end
    end
end

-- check flag
-- @see https://github.com/xmake-io/xmake/issues/3594
function check_flag(target, toolinst, flagkind, flag)
    local extraconf = target:extraconf(flagkind)
    flag = target_utils.flag_belong_to_tool(flag, toolinst, extraconf)
    if flag then
        extraconf = extraconf and extraconf[flag]
        if not extraconf or not extraconf.force then
            return toolinst:has_flags(flag)
        end
    end
    return true
end

-- check api configuration in targets
function check_targets(apiname, opt)
    opt = opt or {}
    _check_instances(apiname, opt.target, project.targets, opt)
end

-- check api configuration in packages
function check_packages(apiname, opt)
    opt = opt or {}
    _check_instances(apiname, opt.package, _get_project_packages, opt)
end