summaryrefslogtreecommitdiff
blob: e0370cc3d1d4394559f2973caf49636af14b2500 (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
import re

from c_analyzer_common.info import UNKNOWN

from .info import Variable
from .preprocessor import _iter_clean_lines


_NOT_SET = object()


def get_srclines(filename, *,
                 cache=None,
                 _open=open,
                 _iter_lines=_iter_clean_lines,
                 ):
    """Return the file's lines as a list.

    Each line will have trailing whitespace removed (including newline).

    If a cache is given the it is used.
    """
    if cache is not None:
        try:
            return cache[filename]
        except KeyError:
            pass

    with _open(filename) as srcfile:
        srclines = [line
                    for _, line in _iter_lines(srcfile)
                    if not line.startswith('#')]
    for i, line in enumerate(srclines):
        srclines[i] = line.rstrip()

    if cache is not None:
        cache[filename] = srclines
    return srclines


def parse_variable_declaration(srcline):
    """Return (name, decl) for the given declaration line."""
    # XXX possible false negatives...
    decl, sep, _ = srcline.partition('=')
    if not sep:
        if not srcline.endswith(';'):
            return None, None
        decl = decl.strip(';')
    decl = decl.strip()
    m = re.match(r'.*\b(\w+)\s*(?:\[[^\]]*\])?$', decl)
    if not m:
        return None, None
    name = m.group(1)
    return name, decl


def parse_variable(srcline, funcname=None):
    """Return a Variable for the variable declared on the line (or None)."""
    line = srcline.strip()

    # XXX Handle more than just static variables.
    if line.startswith('static '):
        if '(' in line and '[' not in line:
            # a function
            return None, None
        return parse_variable_declaration(line)
    else:
        return None, None


def iter_variables(filename, *,
                   srccache=None,
                   parse_variable=None,
                   _get_srclines=get_srclines,
                   _default_parse_variable=parse_variable,
                   ):
    """Yield a Variable for each in the given source file."""
    if parse_variable is None:
        parse_variable = _default_parse_variable

    indent = ''
    prev = ''
    funcname = None
    for line in _get_srclines(filename, cache=srccache):
        # remember current funcname
        if funcname:
            if line == indent + '}':
                funcname = None
                continue
        else:
            if '(' in prev and line == indent + '{':
                if not prev.startswith('__attribute__'):
                    funcname = prev.split('(')[0].split()[-1]
                    prev = ''
                    continue
            indent = line[:-len(line.lstrip())]
            prev = line

        info = parse_variable(line, funcname)
        if isinstance(info, list):
            for name, _funcname, decl in info:
                yield Variable.from_parts(filename, _funcname, name, decl)
            continue
        name, decl = info

        if name is None:
            continue
        yield Variable.from_parts(filename, funcname, name, decl)


def _match_varid(variable, name, funcname, ignored=None):
    if ignored and variable in ignored:
        return False

    if variable.name != name:
        return False

    if funcname == UNKNOWN:
        if not variable.funcname:
            return False
    elif variable.funcname != funcname:
        return False

    return True


def find_variable(filename, funcname, name, *,
                  ignored=None,
                  srccache=None,  # {filename: lines}
                  parse_variable=None,
                  _iter_variables=iter_variables,
                  ):
    """Return the matching variable.

    Return None if the variable is not found.
    """
    for variable in _iter_variables(filename,
                                    srccache=srccache,
                                    parse_variable=parse_variable,
                                    ):
        if _match_varid(variable, name, funcname, ignored):
            return variable
    else:
        return None


def find_variables(varids, filenames=None, *,
                   srccache=_NOT_SET,
                   parse_variable=None,
                   _find_symbol=find_variable,
                   ):
    """Yield a Variable for each ID.

    If the variable is not found then its decl will be UNKNOWN.  That
    way there will be one resulting Variable per given ID.
    """
    if srccache is _NOT_SET:
        srccache = {}

    used = set()
    for varid in varids:
        if varid.filename and varid.filename != UNKNOWN:
            srcfiles = [varid.filename]
        else:
            if not filenames:
                yield Variable(varid, UNKNOWN)
                continue
            srcfiles = filenames
        for filename in srcfiles:
            found = _find_varid(filename, varid.funcname, varid.name,
                                 ignored=used,
                                 srccache=srccache,
                                 parse_variable=parse_variable,
                                 )
            if found:
                yield found
                used.add(found)
                break
        else:
            yield Variable(varid, UNKNOWN)