blob: 4f41808b5bf99e9fb14ce9ecee5bcb6c0d2b811e [file] [log] [blame] [edit]
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2017, Data61, CSIRO (ABN 41 687 119 230)
#
# SPDX-License-Identifier: BSD-2-Clause
#
'''
Cscope for CAmkES. Pass --help for usage instructions.
'''
from __future__ import absolute_import, division, print_function, \
unicode_literals
import argparse, os, pycscope, subprocess, sys
MY_DIR = os.path.abspath(os.path.dirname(__file__))
# Make the CAmkES parser importable.
sys.path.append(os.path.join(MY_DIR, '..'))
from camkes.ast import Assembly, Component, Composition, Configuration, Connection, Instance, Reference, Setting
from camkes.parser.stage0 import CPP, Reader
from camkes.parser.stage1 import Parse1
from camkes.parser.stage2 import Parse2
from camkes.parser.stage3 import Parse3
class Import(object):
def __init__(self, path):
self.path = path
class Item(object):
def __init__(self, start_lineno, start_col, end_lineno, end_col, content):
self.start_lineno = start_lineno
self.start_col = start_col
self.end_lineno = end_lineno
self.end_col = end_col
self.content = content
class Bucket(object):
def __init__(self, source):
self.pending = []
self.source = source
def add(self, item):
self.pending.append(item)
def to_cscope(self):
lines = self.source.split('\n')
lineno = 1
col = 1
line = pycscope.Line(lineno)
for item in sorted(self.pending):
while item.start_lineno > lineno:
interloper = lines[lineno - 1][col - 1:]
if interloper != '':
line += pycscope.NonSymbol(interloper)
yield line
lineno += 1
col = 1
line = pycscope.Line(lineno)
if item.start_col > col:
interloper = lines[lineno - 1][col - 1:item.start_col - 1]
assert interloper != ''
line += pycscope.NonSymbol(interloper)
col += len(interloper)
assert item.start_col == col
content = item.content
# The following logic defines how we map CAmkES entities to the C
# entities understood by CScope. Note that we map almost everything
# to function semantics and leave the user to disambiguate.
if isinstance(content, Assembly):
assert content.name is not None
line += pycscope.Symbol(content.name, pycscope.Mark.FUNC_DEF)
elif isinstance(content, Component):
assert content.name is not None
line += pycscope.Symbol(content.name, pycscope.Mark.FUNC_DEF)
elif isinstance(content, Composition):
assert content.name is not None
line += pycscope.Symbol(content.name, pycscope.Mark.FUNC_DEF)
elif isinstance(content, Configuration):
assert content.name is not None
line += pycscope.Symbol(content.name, pycscope.Mark.FUNC_DEF)
elif isinstance(content, Connection):
line += pycscope.Symbol(content.name, pycscope.Mark.GLOBAL)
elif isinstance(content, Import):
line += pycscope.Symbol(content.path, pycscope.Mark.INCLUDE)
elif isinstance(content, Instance):
line += pycscope.Symbol(content.name, pycscope.Mark.GLOBAL)
elif isinstance(content, Reference):
line += pycscope.Symbol(content.name, pycscope.Mark.FUNC_CALL)
elif isinstance(content, Setting):
line += pycscope.Symbol('%s.%s' % (content.instance,
content.attribute), pycscope.Mark.ASSIGN)
def _get_files(dirname):
assert os.path.isdir(dirname)
for root, _, files in os.walk(dirname):
for f in files:
if os.path.splitext(f)[-1].lower() == '.camkes':
yield os.path.abspath(os.path.join(root, f))
def get_files(path):
if os.path.isdir(path):
for f in _get_files(path):
yield os.path.split(f[len(os.path.commonprefix([f, path])):])
else:
yield os.path.split(path)
def cross_references(path, options):
index = []
# Setup a stage 0 parser, to optionally CPP the input.
if options.cpp:
s0 = CPP(options.toolprefix, options.cpp_flag)
else:
s0 = Reader()
# Setup a stage 1 parser to translate the source(s) into a plyplus AST.
s1 = Parse1(s0)
# Setup a stage 2 parser, though we will not actually utilise its
# functionality. We'll strip the import statements before going beyond
# stage 1 to mask any subordinate parse errors.
s2 = Parse2(s1)
# Setup a stage 3 parser. This is as far as we will go, and we only go to
# this point in order to take advantage of the `LiftedAST` representation.
s3 = Parse3(s2)
for base, name in get_files(path):
abspath = os.path.join(base, name)
# File index entry.
yield '\n%s%s\n\n' % (pycscope.Mark(pycscope.Mark.FILE),
abspath), abspath
_, ast, _ = s1.parse_file(os.path.join(base, name))
# We want to eventually parse this to a proper AST, but we first need
# to handle import statements as we don't want to actually import any
# extra files.
def build_cross_reference(path, options):
index = []
files = set()
for result in cross_references(path, options):
if isinstance(result, Warning):
sys.stderr.write('warning: %s\n' % str(result))
else:
entry, file = result
index.append(entry)
files.add(file)
return index, list(files)
def main(argv):
parser = argparse.ArgumentParser(
description='Cscope for CAmkES specifications')
parser.add_argument('--cpp', action='store_true', help='Pre-process the '
'source with CPP')
parser.add_argument('--nocpp', action='store_false', dest='cpp',
help='Do not pre-process the source with CPP')
parser.add_argument('--toolprefix', help='compiler toolchain prefix',
default='')
parser.add_argument('--cpp-flag', action='append', default=[],
help='Specify a flag to pass to CPP')
parser.add_argument('--import-path', '-I', help='Add this path to the list '
'of paths to search for built-in imports. That is, add it to the list '
'of directories that are searched to find the file "foo" when '
'encountering an expression "import <foo>;".', action='append',
default=[])
parser.add_argument('--build-cross-reference-only', '-b',
action='store_true', help='Build the cross-reference only')
parser.add_argument('--ignore-case', '-C', action='store_true',
help='Ignore the letter case when searching')
parser.add_argument('--no-update', '-d', action='store_true', help='Do '
'not update the cross-reference')
parser.add_argument('--reference-file', '-f', type=argparse.FileType('w'),
help='Use FILE as the cross-reference file name instead of the '
'default "camkes-scope.out".', default=open('camkes-scope.out', 'wt'))
parser.add_argument('source', default=os.getcwd(), help='file or '
'directory to scan', nargs='?')
opts = parser.parse_args(argv[1:])
if not os.path.exists(opts.source):
sys.stderr.write('%s does not exist\n' % opts.source)
return -1
# Set up a default import path to help the user out.
if len(opts.import_path) == 0:
opts.import_path.append(os.path.join(MY_DIR, '../include/builtin'))
if not opts.no_update:
index, files = build_cross_reference(opts.source, opts)
if os.path.isdir(opts.source):
target = opts.source
else:
target = os.path.dirname(opts.source)
base = os.path.relpath(target,
os.path.dirname(opts.reference_file.name))
pycscope.writeIndex(base, opts.reference_file, index, files)
if opts.build_cross_reference_only:
return 0
args = []
if opts.ignore_case:
args.append('-C')
# Run Cscope.
return subprocess.call(['cscope', '-d', '-f%s' % opts.reference_file.name] +
args)
if __name__ == '__main__':
sys.exit(main(sys.argv))