<gh_stars>0
"""Quick and dirty URL checker."""
import typing as t
import uuid
from inspect import Parameter, _empty, signature  # type: ignore[attr-defined]

from django.conf import settings
from django.core import checks
from django.urls import URLPattern, URLResolver, converters, get_resolver
from django.urls.resolvers import RoutePattern


@checks.register(checks.Tags.urls)
def check_url_signatures(app_configs, **kwargs) -> t.List[t.Union[checks.Error, checks.Warning]]:
    """Check that all callbacks in the main urlconf have the correct signature.

    Args:
        app_configs: A list of AppConfig instances that will be checked.
        **kwargs: Not used.

    Returns:
        A list of errors.
    """
    if not getattr(settings, 'ROOT_URLCONF', None):
        return []

    resolver = get_resolver()
    errors = []
    for route in get_all_routes(resolver):
        errors.extend(check_url_args_match(route))
    return errors


def get_all_routes(resolver: URLResolver) -> t.Iterable[URLPattern]:
    """Recursively get all routes from the resolver."""
    if resolver.app_name == 'admin':
        return []
    for pattern in resolver.url_patterns:
        if isinstance(pattern, URLResolver):
            yield from get_all_routes(pattern)
        else:
            if isinstance(pattern.pattern, RoutePattern):
                yield pattern


def check_url_args_match(url_pattern: URLPattern) -> t.List[t.Union[checks.Error, checks.Warning]]:
    """Check that all callbacks in the main urlconf have the correct signature."""
    callback = url_pattern.callback
    callback_repr = f'{callback.__module__}.{callback.__qualname__}'
    errors = []
    sig = signature(callback)
    parameters = sig.parameters

    has_star_args = False
    if any(p.kind in [Parameter.VAR_KEYWORD, Parameter.VAR_POSITIONAL] for p in parameters.values()):
        errors.append(
            checks.Warning(
                f'View {callback_repr} signature contains *args or **kwarg syntax, can\'t properly check args',
                obj=url_pattern,
                id='urlchecker.W001',
            )
        )
        has_star_args = True

    used_from_sig = []
    parameter_list = list(sig.parameters)
    if parameter_list and parameter_list[0] == 'self':
        # HACK: we need to find some nice way to detect closures/bound methods,
        # while also getting the final signature.
        parameter_list.pop(0)
        used_from_sig.append('self')

    if not parameter_list or parameter_list[0] != 'request':
        if not has_star_args:
            if parameter_list:
                message = (
                    f'View {callback_repr} signature does not start with `request` parameter, '
                    f'found `{parameter_list[0]}`.'
                )
            else:
                message = f'View {callback_repr} signature does not have `request` parameter.'
            errors.append(
                checks.Error(
                    message,
                    obj=url_pattern,
                    id='urlchecker.E001',
                )
            )
    else:
        used_from_sig.append('request')

    # Everything in RoutePattern must be in signature
    for name, converter in url_pattern.pattern.converters.items():
        if has_star_args:
            used_from_sig.append(name)
        elif name in sig.parameters:
            used_from_sig.append(name)
            expected_type = get_converter_output_type(converter)
            found_type = sig.parameters[name].annotation
            if expected_type == Parameter.empty:
                # TODO - only output this warning once per converter
                errors.append(
                    checks.Warning(
                        f'Don\'t know output type for convert {converter}, can\'t verify URL signatures.',
                        obj=converter,
                        id=f'urlchecker.W002.{converter.__module__}.{converter.__class__.__name__}',
                    )
                )
            elif found_type == Parameter.empty:
                errors.append(
                    checks.Warning(
                        f'Missing type annotation for parameter `{name}`, can\'t check type.',
                        obj=url_pattern,
                        id='urlchecker.W003',
                    )
                )
            elif expected_type != found_type:
                errors.append(
                    checks.Error(
                        f'For parameter `{name}`,'  # type: ignore[union-attr]
                        f' annotated type {found_type.__name__} does not match'  # type: ignore[union-attr]
                        f' expected `{expected_type.__name__}` from urlconf',  # type: ignore[union-attr]
                        obj=url_pattern,
                        id='urlchecker.E002',
                    )
                )
        else:
            errors.append(
                checks.Error(
                    f'View {callback_repr} signature does not contain `{name}` parameter',
                    obj=url_pattern,
                    id='urlchecker.E003',
                )
            )

    # Anything left over must have a default argument
    for name, param in sig.parameters.items():
        if name in used_from_sig:
            continue
        if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
            continue
        if param.default == Parameter.empty:
            errors.append(
                checks.Error(
                    f'View {callback_repr} signature contains `{name}` parameter without default or ULRconf parameter',
                    obj=url_pattern,
                    id='urlchecker.E004',
                )
            )

    return errors


CONVERTER_TYPES = {
    converters.IntConverter: int,
    converters.StringConverter: str,
    converters.UUIDConverter: uuid.UUID,
    converters.SlugConverter: str,
    converters.PathConverter: str,
}


def get_converter_output_type(converter) -> t.Union[int, str, uuid.UUID, t.Type[_empty]]:
    """Return the type that the converter will output."""
    cls = type(converter)
    if cls in CONVERTER_TYPES:
        return CONVERTER_TYPES[cls]

    sig = signature(converter.to_python)
    if sig.return_annotation != Parameter.empty:
        return sig.return_annotation

    return Parameter.empty
