Подтвердить что ты не робот

Предупреждение о недостатке запятой между ошибкой элементов списка

История:

Когда список строк определен на нескольких строках, часто легко забыть запятую между элементами списка, например, в этом примере:

test = [
    "item1"
    "item2"
]

В списке test теперь будет один элемент "item1item2".

Довольно часто проблема возникает после переупорядочения элементов в списке.

Вопросы о переполнении стека, имеющие эту проблему:

Вопрос:

Есть ли способ, предпочтительно, используя статический анализ кода, выдать предупреждение в таких случаях, чтобы выявить проблему как можно раньше?

4b9b3361

Ответ 1

Это всего лишь вероятные решения, так как я не очень склонен к статическому анализу.

С tokenize:

Недавно я искал с токенизацией кода python, и я считаю, что у него есть вся информация, необходимая для выполнения таких проверок, когда достаточная логика добавлено. Для данного списка токены, сгенерированные с помощью python -m tokenize list1.py, выглядят следующим образом:

python -m tokenize list1.py 

1,0-1,4:    NAME    'test'
1,5-1,6:    OP  '='
1,7-1,8:    OP  '['
1,8-1,9:    NL  '\n'
2,1-2,8:    STRING  '"item1"'
2,8-2,9:    NL  '\n'
3,1-3,8:    STRING  '"item2"'
3,8-3,9:    NL  '\n'
4,0-4,1:    OP  ']'
4,1-4,2:    NEWLINE '\n'
5,0-5,0:    ENDMARKER   ''

Это, конечно, "проблемный" случай, когда содержимое собирается конкатенироваться. В случае, когда a , присутствует, результат слегка изменяется, чтобы отразить это (я добавил токены только для тела списка):

1,7-1,8:    OP  '['
1,8-1,9:    NL  '\n'
2,1-2,8:    STRING  '"item1"'
2,8-2,9:    OP  ','
2,9-2,10:   NL  '\n'
3,1-3,8:    STRING  '"item2"'
3,8-3,9:    NL  '\n'
4,0-4,1:    OP  ']'

Теперь у нас есть дополнительный знак OP ',', обозначающий наличие второго элемента, разделенного запятой.

Учитывая эту информацию, мы могли бы использовать действительно удобный метод generate_tokens в модуле tokenize. Метод tokenize.generate_tokens(), tokenize.tokenize() в Py3, имеет один аргумент readline, метод для файловых объектов, который по существу возвращает следующую строку для такого файла, как объект (соответствующий ответ). Он возвращает именованный кортеж с 5 элементами в общей сложности с информацией о типе токена, символьной строкой вместе с номером строки и позицией в строке.

Используя эту информацию, теоретически можно прокрутить файл и когда OP ',' отсутствует внутри инициализации списка (начало которого определяется путем проверки того, что токены NAME, OP '=' и OP '[' существуют на номер той же строки) можно выдать предупреждение о строках, на которых оно было обнаружено.

Хорошо, что этот подход заключается в том, что он довольно прямолинейный для обобщения. Чтобы соответствовать всем случаям, когда имеет место конкатенация строковых литералов (а именно, внутри операторов группировки (), {}, []), вы проверяете, что токен имеет type = 51 (или 53 для Python 3) или что значение в любом из (, [, { существует в одной строке (это грубые, верхняя часть головы предложения atm).

Теперь я не совсем уверен, как другие люди занимаются такими проблемами , но кажется, что это может быть то, что вы можете изучить. Вся необходимая информация предоставляется tokenize, логика обнаружения - это единственное, что отсутствует.

Замечание по реализации: Эти значения (например, для type) различаются между версиями и могут быть изменены, поэтому об этом нужно знать. Можно было бы использовать этот только работая с константами для токенов.


С parser и ast:

Еще одно вероятное решение, которое, вероятно, более утомительно, может включать parser и ast. Конкатенация строк фактически выполняется во время создания абстрактного дерева синтаксиса, чтобы вы могли альтернативно обнаружить его там.

Я действительно не хочу выгружать полный вывод методов для parser и ast, что я собираюсь упомянуть, но, чтобы убедиться, что мы на одной странице, я будет использоваться следующий оператор инициализации списка:

l_init = """
test = [
    "item1"
    "item2",
    "item3"
]
"""

Чтобы получить генерируемое дерево разбора, используйте p = parser.suite(l_init). После этого вы можете просмотреть его с помощью p.tolist() (слишком большой вывод, чтобы добавить его). Вы заметите, что будут три записи для трех разных объектов str item1, item2, item3.

С другой стороны, когда AST создается с помощью node = ast.parse(l_init) и просматривается с помощью ast.dump(node) есть только две записи: одна для конкатенированного str item1item2 и другая для другой записи item3.

Итак, это еще один вероятный способ сделать это, но, как я уже упоминал ранее, это еще более утомительно. Я не уверен, доступна ли линейная информация, и вы имеете дело с двумя разными модулями. Просто подумайте, если вы, возможно, захотите поиграть с внутренними объектами выше в цепочке компилятора.


Закрытие комментариев. Как заключительная записка, подход tokenize представляется наиболее логичным в этом случае. Наоборот, похоже, что pylint работает с astroidpython lib, которая упрощает анализ абстрактных синтаксических деревьев для кода python. Итак, в идеале нужно взглянуть на него и как он используется внутри pylint.

Примечание. Конечно, я мог бы полностью переубедить его, и более простое решение для решения "белого пространства" или новой строки, как вам было предложено, было бы достаточно.: -)

Ответ 2

Я реализовал код на основе сообщения @Jim. Может ли он работать во всех ситуациях:

import tokenize
from io import BytesIO

def my_checker(pycode):
    """
    tokenizes python code and yields 
    start, end, strline of any position where 
    a scenario like this happens (missing string seperator):
      [..., "a string" "derp", ...]
    """
    IDLE = 0
    WAITING_STRING = 1
    CHECKING_SEPARATOR = 2

    tokenizer = tokenize.tokenize(BytesIO(pycode.encode('utf-8')).readline)
    state = IDLE

    for toknum, tokval, start, end, strcode  in tokenizer:
        if state == IDLE:
            if toknum == tokenize.OP and tokval == '[':
                state = WAITING_STRING

        elif state == WAITING_STRING:
            if toknum == tokenize.STRING:
                state = CHECKING_SEPARATOR
            elif toknum == tokenize.OP and tokval == [']']:
                state = IDLE

        elif state == CHECKING_SEPARATOR:
            if toknum == tokenize.STRING:
                yield (start, end, strcode)
            elif toknum == tokenize.OP and tokval in ['+', ',']:
                state = WAITING_STRING
            elif toknum == tokenize.OP and tokval == ']':
                state = IDLE

my_code = """
foo = "derp"
def derp(a,x): 
    return str('dingdong'+str(a*x))
[
    "derp"+"FOO22"  , "FOO", "donk" "slurp",0, 0
]

class extreme_logical_class():
    STATIC_BAD_LIST = [0,
        "BLA,",
        "FOO"
        "derp"
    ] 
    def __init__(self):
        self._in_method_check = ["A" "B"]

nested_list = [
    ['DERP','FOO'],
    [0,'hello', 'peter' 'pan'],
    ['this', 'is', ['ultra', 'mega'
        'nested']] 
]
"""

for error in my_checker(my_code):
    print('missing , in list at: line {}@{} to line {}@{}: "{}"'.format(
        error[0][0],error[0][1],error[1][0],error[1][1], error[2].strip()
    ))

Результат:

[email protected] ~ % python3 find_bad_lists.py
missing , in list at: line [email protected] to line [email protected]: ""derp"+"FOO22"  , "FOO", "donk" "blurp",0 0"
missing , in list at: line [email protected] to line [email protected]: ""derp""
missing , in list at: line [email protected] to line [email protected]: "self._in_method_check = ["A" "B"]"
missing , in list at: line [email protected] to line [email protected]: "[0,'hello', 'peter' 'pan'],"
missing , in list at: line [email protected] to line [email protected]: "'nested']]"

В реальной жизни я предпочел бы избегать таких ошибок; есть хорошая среда IDE, например Sublime Text, которая позволяет вам редактировать и форматировать списки с помощью нескольких курсоров. Если вы привыкнете к этим концепциям, эти типы "разделяющих" ошибок не будут происходить в вашем коде.

Конечно, если у команды разработчиков есть возможность интегрировать такой инструмент в среду тестирования.

Ответ 3

Это регулярное выражение найдет вхождения проблемы. Просто найдите все файлы в своем проекте.

\[("[^"]*",[\s]*)*"[^"]*"[\s]*"

Протестировано в https://regex101.com/ и NotePad ++