cpython/Tools/c-analyzer/c_analyzer/analyze.py

from c_parser.info import (
    KIND,
    TypeDeclaration,
    POTSType,
    FuncPtr,
)
from c_parser.match import (
    is_pots,
    is_funcptr,
)
from .info import (
    IGNORED,
    UNKNOWN,
    SystemType,
)
from .match import (
    is_system_type,
)


def get_typespecs(typedecls):
    typespecs = {}
    for decl in typedecls:
        if decl.shortkey not in typespecs:
            typespecs[decl.shortkey] = [decl]
        else:
            typespecs[decl.shortkey].append(decl)
    return typespecs


def analyze_decl(decl, typespecs, knowntypespecs, types, knowntypes, *,
                 analyze_resolved=None):
    resolved = resolve_decl(decl, typespecs, knowntypespecs, types)
    if resolved is None:
        # The decl is supposed to be skipped or ignored.
        return None
    if analyze_resolved is None:
        return resolved, None
    return analyze_resolved(resolved, decl, types, knowntypes)

# This alias helps us avoid name collisions.
_analyze_decl = analyze_decl


def analyze_type_decls(types, analyze_decl, handle_unresolved=True):
    unresolved = set(types)
    while unresolved:
        updated = []
        for decl in unresolved:
            resolved = analyze_decl(decl)
            if resolved is None:
                # The decl should be skipped or ignored.
                types[decl] = IGNORED
                updated.append(decl)
                continue
            typedeps, _ = resolved
            if typedeps is None:
                raise NotImplementedError(decl)
            if UNKNOWN in typedeps:
                # At least one dependency is unknown, so this decl
                # is not resolvable.
                types[decl] = UNKNOWN
                updated.append(decl)
                continue
            if None in typedeps:
                # XXX
                # Handle direct recursive types first.
                nonrecursive = 1
                if decl.kind is KIND.STRUCT or decl.kind is KIND.UNION:
                    nonrecursive = 0
                    i = 0
                    for member, dep in zip(decl.members, typedeps):
                        if dep is None:
                            if member.vartype.typespec != decl.shortkey:
                                nonrecursive += 1
                            else:
                                typedeps[i] = decl
                        i += 1
                if nonrecursive:
                    # We don't have all dependencies resolved yet.
                    continue
            types[decl] = resolved
            updated.append(decl)
        if updated:
            for decl in updated:
                unresolved.remove(decl)
        else:
            # XXX
            # Handle indirect recursive types.
            ...
            # We couldn't resolve the rest.
            # Let the caller deal with it!
            break
    if unresolved and handle_unresolved:
        if handle_unresolved is True:
            handle_unresolved = _handle_unresolved
        handle_unresolved(unresolved, types, analyze_decl)


def resolve_decl(decl, typespecs, knowntypespecs, types):
    if decl.kind is KIND.ENUM:
        typedeps = []
    else:
        if decl.kind is KIND.VARIABLE:
            vartypes = [decl.vartype]
        elif decl.kind is KIND.FUNCTION:
            vartypes = [decl.signature.returntype]
        elif decl.kind is KIND.TYPEDEF:
            vartypes = [decl.vartype]
        elif decl.kind is KIND.STRUCT or decl.kind is KIND.UNION:
            vartypes = [m.vartype for m in decl.members]
        else:
            # Skip this one!
            return None

        typedeps = []
        for vartype in vartypes:
            typespec = vartype.typespec
            if is_pots(typespec):
                typedecl = POTSType(typespec)
            elif is_system_type(typespec):
                typedecl = SystemType(typespec)
            elif is_funcptr(vartype):
                typedecl = FuncPtr(vartype)
            else:
                typedecl = find_typedecl(decl, typespec, typespecs)
                if typedecl is None:
                    typedecl = find_typedecl(decl, typespec, knowntypespecs)
                elif not isinstance(typedecl, TypeDeclaration):
                    raise NotImplementedError(repr(typedecl))
                if typedecl is None:
                    # We couldn't find it!
                    typedecl = UNKNOWN
                elif typedecl not in types:
                    # XXX How can this happen?
                    typedecl = UNKNOWN
                elif types[typedecl] is UNKNOWN:
                    typedecl = UNKNOWN
                elif types[typedecl] is IGNORED:
                    # We don't care if it didn't resolve.
                    pass
                elif types[typedecl] is None:
                    # The typedecl for the typespec hasn't been resolved yet.
                    typedecl = None
            typedeps.append(typedecl)
    return typedeps


def find_typedecl(decl, typespec, typespecs):
    specdecls = typespecs.get(typespec)
    if not specdecls:
        return None

    filename = decl.filename

    if len(specdecls) == 1:
        typedecl, = specdecls
        if '-' in typespec and typedecl.filename != filename:
            # Inlined types are always in the same file.
            return None
        return typedecl

    # Decide which one to return.
    candidates = []
    samefile = None
    for typedecl in specdecls:
        type_filename = typedecl.filename
        if type_filename == filename:
            if samefile is not None:
                # We expect type names to be unique in a file.
                raise NotImplementedError((decl, samefile, typedecl))
            samefile = typedecl
        elif filename.endswith('.c') and not type_filename.endswith('.h'):
            # If the decl is in a source file then we expect the
            # type to be in the same file or in a header file.
            continue
        candidates.append(typedecl)
    if not candidates:
        return None
    elif len(candidates) == 1:
        winner, = candidates
        # XXX Check for inline?
    elif '-' in typespec:
        # Inlined types are always in the same file.
        winner = samefile
    elif samefile is not None:
        # Favor types in the same file.
        winner = samefile
    else:
        # We don't know which to return.
        raise NotImplementedError((decl, candidates))

    return winner


#############################
# handling unresolved decls

class Skipped(TypeDeclaration):
    def __init__(self):
        _file = _name = _data = _parent = None
        super().__init__(_file, _name, _data, _parent, _shortkey='<skipped>')
_SKIPPED = Skipped()
del Skipped


def _handle_unresolved(unresolved, types, analyze_decl):
    #raise NotImplementedError(unresolved)

    dump = True
    dump = False
    if dump:
        print()
    for decl in types:  # Preserve the original order.
        if decl not in unresolved:
            assert types[decl] is not None, decl
            if types[decl] in (UNKNOWN, IGNORED):
                unresolved.add(decl)
                if dump:
                    _dump_unresolved(decl, types, analyze_decl)
                    print()
            else:
                assert types[decl][0] is not None, (decl, types[decl])
                assert None not in types[decl][0], (decl, types[decl])
        else:
            assert types[decl] is None
            if dump:
                _dump_unresolved(decl, types, analyze_decl)
                print()
    #raise NotImplementedError

    for decl in unresolved:
        types[decl] = ([_SKIPPED], None)

    for decl in types:
        assert types[decl]


def _dump_unresolved(decl, types, analyze_decl):
    if isinstance(decl, str):
        typespec = decl
        decl, = (d for d in types if d.shortkey == typespec)
    elif type(decl) is tuple:
        filename, typespec = decl
        if '-' in typespec:
            found = [d for d in types
                     if d.shortkey == typespec and d.filename == filename]
            #if not found:
            #    raise NotImplementedError(decl)
            decl, = found
        else:
            found = [d for d in types if d.shortkey == typespec]
            if not found:
                print(f'*** {typespec} ???')
                return
                #raise NotImplementedError(decl)
            else:
                decl, = found
    resolved = analyze_decl(decl)
    if resolved:
        typedeps, _ = resolved or (None, None)

    if decl.kind is KIND.STRUCT or decl.kind is KIND.UNION:
        print(f'*** {decl.shortkey} {decl.filename}')
        for member, mtype in zip(decl.members, typedeps):
            typespec = member.vartype.typespec
            if typespec == decl.shortkey:
                print(f'     ~~~~: {typespec:20} - {member!r}')
                continue
            status = None
            if is_pots(typespec):
                mtype = typespec
                status = 'okay'
            elif is_system_type(typespec):
                mtype = typespec
                status = 'okay'
            elif mtype is None:
                if '-' in member.vartype.typespec:
                    mtype, = [d for d in types
                              if d.shortkey == member.vartype.typespec
                              and d.filename == decl.filename]
                else:
                    found = [d for d in types
                             if d.shortkey == typespec]
                    if not found:
                        print(f' ???: {typespec:20}')
                        continue
                    mtype, = found
            if status is None:
                status = 'okay' if types.get(mtype) else 'oops'
            if mtype is _SKIPPED:
                status = 'okay'
                mtype = '<skipped>'
            elif isinstance(mtype, FuncPtr):
                status = 'okay'
                mtype = str(mtype.vartype)
            elif not isinstance(mtype, str):
                if hasattr(mtype, 'vartype'):
                    if is_funcptr(mtype.vartype):
                        status = 'okay'
                mtype = str(mtype).rpartition('(')[0].rstrip()
            status = '    okay' if status == 'okay' else f'--> {status}'
            print(f' {status}: {typespec:20} - {member!r} ({mtype})')
    else:
        print(f'*** {decl} ({decl.vartype!r})')
        if decl.vartype.typespec.startswith('struct ') or is_funcptr(decl):
            _dump_unresolved(
                (decl.filename, decl.vartype.typespec),
                types,
                analyze_decl,
            )