def generate_pylint_comments(in_path):
    '''
        Use Pylint to generate additional feedback comments for code,

        e.g. if code follows PEP8 Style Convention
    '''

    pylint_stdout, _ = lint.py_run(
        str(in_path) +
        ' --score=no --msg-template="{category}, {line}, {msg_id} {symbol}, {msg}"',
        return_std=True)

    status_mapping = {
        'informational': CommentTypes.INFORMATIVE,
        'refactor': CommentTypes.ACTIONABLE,
        'convention': CommentTypes.ACTIONABLE,
        'warning': CommentTypes.ESSENTIAL,
        'error': CommentTypes.ESSENTIAL,
        'fatal': CommentTypes.ESSENTIAL
    }

    cleaned_pylint_output = [
        tuple(item.strip('" ').split(', '))
        for item in pylint_stdout.getvalue().splitlines() if '**' not in item
    ]

    pylint_comments = []

    for line in cleaned_pylint_output:
        if line[0]:
            if line[2] == "C0301 line-too-long":
                continue

            if line[2] in ("C0114 missing-module-docstring",
                           "C0116 missing-function-docstring",
                           "C0304 missing-final-newline"):

                pylint_comments.append(
                    Comment(type=status_mapping['informational'],
                            params={
                                'lineno': line[1],
                                'code': line[2],
                                'message': line[3:]
                            },
                            comment=f'python.pylint.{line[0]}'))
            else:
                pylint_comments.append(
                    Comment(type=status_mapping[line[0]],
                            params={
                                'lineno': line[1],
                                'code': line[2],
                                'message': line[3:]
                            },
                            comment=f'python.pylint.{line[0]}'))

    return pylint_comments
Example #2
0
def analyze(in_path: Path, out_path: Path):
    """Analyze the user's solution and give feedback.

    Outputs a JSON that conforms to
    https://github.com/exercism/docs/blob/main/building/tooling/analyzers/interface.md#output-format
    """

    # List of Comment objects to process
    comments = []

    output_file = out_path.parent.joinpath("analysis.json")

    # input file - if it can't be found, fail and bail
    try:
        user_solution = in_path.read_text()
    except OSError as err:
        # fail out fast with an ESSENTIAL (required) type comment for the student
        comments.append(
            Comment(type=CommentTypes.ESSENTIAL,
                    params={},
                    comment=Comments.MALFORMED_CODE))
    finally:
        if comments:
            return Analysis.require(comments).dump(output_file)

    # AST - if an AST can't be made, fail and bail
    try:
        tree = ast.parse(user_solution)
    except Exception:
        # If ast.parse fails, assume malformed code and fail with an ESSENTIAL (required) type comment for the student
        comments.append(
            Comment(type=CommentTypes.ESSENTIAL,
                    params={},
                    comment=Comments.MALFORMED_CODE))
    finally:
        if comments:
            return Analysis.require(comments).dump(output_file)

    # Generate PyLint comments for additional feedback.
    comments.extend(
        generate_pylint_comments(
            in_path,
            pylint_spec='/opt/analyzer/lib/ellens-alien-game/.pylintrc'))

    return Analysis.summarize_comments(comments, output_file)
Example #3
0
def analyze(in_path: Path, out_path: Path):
    """
    Analyze the user's Two Fer solution to give feedback. Outputs a JSON that

    conforms to https://github.com/exercism/docs/blob/main/building/tooling/analyzers/interface.md#output-format
    """

    # List of Comment objects to process
    comments = []

    output_file = out_path.parent.joinpath("analysis.json")

    # input file - if it can't be found, fail and bail
    try:
        user_solution = in_path.read_text()
    except OSError as err:
        # fail out fast with an ESSENTIAL (required) type comment for the student
        comments.append(
            Comment(type=CommentTypes.ESSENTIAL,
                    params={},
                    comment=Comments.MALFORMED_CODE))
    finally:
        if comments:
            return Analysis.require(comments).dump(output_file)

    # AST - if an AST can't be made, fail and bail
    try:
        tree = ast.parse(user_solution)
    except Exception:
        # If ast.parse fails, assume malformed code and fail with an ESSENTIAL (required) type comment for the student
        comments.append(
            Comment(type=CommentTypes.ESSENTIAL,
                    params={},
                    comment=Comments.MALFORMED_CODE))
    finally:
        if comments:
            return Analysis.require(comments).dump(output_file)

    # Does the solution have a method called two_fer?
    has_method = False

    # Does the solution correctly use a default argument?
    uses_def_arg = False

    # Does the solution have a return value?
    has_return = False

    # Does the solution use str.format?
    uses_format = False

    # Does the solution use f-strings?
    uses_f_string = False

    for node in ast.walk(tree):

        # Check for method called two_fer
        if isinstance(node, ast.FunctionDef):
            has_method = node.name == "two_fer"

        # Check for the use of string concatenation with + operator
        elif isinstance(node,
                        ast.Add) and Comments.SIMPLE_CONCAT not in comments:
            comments.append(
                Comment(type=CommentTypes.ACTIONABLE,
                        params={},
                        comment=Comments.SIMPLE_CONCAT))

        # Check for use of default arguments
        elif isinstance(node, ast.arguments):
            if node.defaults:
                uses_def_arg = True
                # Check if the default argument use is correct
                try:
                    if (node.defaults[0].s != "you"
                            and Comments.WRONG_DEF_ARG not in comments):
                        comments.append(
                            Comment(type=CommentTypes.ESSENTIAL,
                                    params={},
                                    comment=Comments.WRONG_DEF_ARG))
                except Exception:
                    if Comments.WRONG_DEF_ARG not in comments:
                        comments.append(
                            Comment(type=CommentTypes.ESSENTIAL,
                                    params={},
                                    comment=Comments.WRONG_DEF_ARG))

        # Check for use of unnecessary conditionals
        elif isinstance(node,
                        ast.If) and Comments.CONDITIONALS not in comments:
            comments.append(
                Comment(type=CommentTypes.ACTIONABLE,
                        params={},
                        comment=Comments.CONDITIONALS))

        # Check for use of %-formatting
        elif isinstance(
                node, ast.Mod) and Comments.PERCENT_FORMATTING not in comments:
            comments.append(
                Comment(type=CommentTypes.ACTIONABLE,
                        params={},
                        comment=Comments.PERCENT_FORMATTING))

        # Check for a return statement
        elif isinstance(node, ast.Return):
            has_return = True

        # Search for use of str.format
        elif isinstance(node, ast.Call):
            try:
                uses_format = node.func.attr == "format"
            except Exception:
                pass

        # Search for use of f-strings
        try:
            if isinstance(node, ast.FormattedValue):
                uses_f_string = True
        except AttributeError:
            pass  # Fail if python version is too low

    if not has_method:
        comments.append(
            Comment(type=CommentTypes.ESSENTIAL,
                    params={},
                    comment=Comments.NO_METHOD))

    if not uses_def_arg:
        comments.append(
            Comment(type=CommentTypes.ESSENTIAL,
                    params={},
                    comment=Comments.NO_DEF_ARG))

    if not has_return:
        comments.append(
            Comment(type=CommentTypes.ESSENTIAL,
                    params={},
                    comment=Comments.NO_RETURN))

    # Generate PyLint comments for additional feedback.
    comments.extend(generate_pylint_comments(in_path))

    # Process all comments and write out feedback.
    if uses_format or uses_f_string:
        return Analysis.summarize_comments(comments, output_file, ideal=True)
    else:
        return Analysis.summarize_comments(comments, output_file)