import ast
from collections import defaultdict
from typing import (
    Callable,
    ClassVar,
    DefaultDict,
    FrozenSet,
    List,
    Tuple,
    Union,
)

from typing_extensions import TypeAlias, final

from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.logic import source, walk
from wemake_python_styleguide.logic.complexity import overuses
from wemake_python_styleguide.logic.tree import annotations
from wemake_python_styleguide.types import AnyNodes, AnyText, AnyTextPrimitive
from wemake_python_styleguide.violations import complexity
from wemake_python_styleguide.visitors import base, decorators

#: We use these types to store the number of nodes usage in different contexts.
_Expressions: TypeAlias = DefaultDict[str, List[ast.AST]]
_FunctionExpressions: TypeAlias = DefaultDict[ast.AST, _Expressions]
_StringConstants: TypeAlias = FrozenSet[Union[str, bytes]]


@final
@decorators.alias('visit_any_string', (
    'visit_Str',
    'visit_Bytes',
))
class StringOveruseVisitor(base.BaseNodeVisitor):
    """
    Restricts repeated usage of the same string constant.

    NB: Some short strings are ignored, as their use is very common and
    forcing assignment would not make much sense (i.e. newlines, "",
    comma, dot).
    """

    _ignored_string_constants: ClassVar[_StringConstants] = frozenset((
        ' ',
        '.',
        ',',
        '',
        '\n',
        '\r\n',
        '\t',
        '|',
        b' ',
        b'.',
        b',',
        b'',
        b'\n',
        b'\r\n',
        b'\t',
    ))

    def __init__(self, *args, **kwargs) -> None:
        """Inits the counter for constants."""
        super().__init__(*args, **kwargs)
        self._string_constants: DefaultDict[
            AnyTextPrimitive, int,
        ] = defaultdict(int)

    def visit_any_string(self, node: AnyText) -> None:
        """Restricts to over-use string constants."""
        self._check_string_constant(node)
        self.generic_visit(node)

    def _check_string_constant(self, node: AnyText) -> None:
        if annotations.is_annotation(node):
            return

        # Some strings are so common, that it makes no sense to check if
        # they are overused.
        if node.s in self._ignored_string_constants:
            return

        self._string_constants[node.s] += 1

    def _post_visit(self) -> None:
        for string, usage_count in self._string_constants.items():
            if usage_count > self.options.max_string_usages:
                self.add_violation(
                    complexity.OverusedStringViolation(
                        text=source.render_string(string) or "''",
                        baseline=self.options.max_string_usages,
                    ),
                )


@final
class ExpressionOveruseVisitor(base.BaseNodeVisitor):
    """Finds overused expressions."""

    _expressions: ClassVar[AnyNodes] = (
        # We do not treat `ast.Attribute`s as expressions
        # because they are too widely used. That's a compromise.
        ast.Assert,
        ast.BoolOp,
        ast.BinOp,
        ast.UnaryOp,
        ast.Call,
        ast.Compare,
        ast.Subscript,
        ast.Lambda,

        ast.DictComp,
        ast.Dict,
        ast.List,
        ast.ListComp,
        ast.Tuple,
        ast.GeneratorExp,
        ast.Set,
        ast.SetComp,
    )

    _ignore_predicates: Tuple[Callable[[ast.AST], bool], ...] = (
        overuses.is_decorator,
        overuses.is_self,
        annotations.is_annotation,
        overuses.is_class_context,
        overuses.is_super_call,
        overuses.is_primitive,
        overuses.is_unary_minus,
    )

    _msg: ClassVar[str] = '{0}; used {1}'

    def __init__(self, *args, **kwargs) -> None:
        """We need to track expression usage in functions and modules."""
        super().__init__(*args, **kwargs)
        self._module_expressions: _Expressions = defaultdict(list)
        self._function_expressions: _FunctionExpressions = defaultdict(
            lambda: defaultdict(list),
        )

    def visit(self, node: ast.AST) -> None:
        """Visits all nodes in a module to find overused values."""
        if isinstance(node, self._expressions):
            self._add_expression(node)
        self.generic_visit(node)

    def _add_expression(self, node: ast.AST) -> None:
        if any(ignore(node) for ignore in self._ignore_predicates):
            return

        source_code = source.node_to_string(node)
        self._module_expressions[source_code].append(node)

        maybe_function = walk.get_closest_parent(node, FunctionNodes)
        if maybe_function is not None:
            self._function_expressions[maybe_function][source_code].append(
                node,
            )

    def _post_visit(self) -> None:
        for mod_source, module_nodes in self._module_expressions.items():
            if len(module_nodes) > self.options.max_module_expressions:
                self.add_violation(
                    complexity.OverusedExpressionViolation(
                        module_nodes[0],
                        text=self._msg.format(mod_source, len(module_nodes)),
                        baseline=self.options.max_module_expressions,
                    ),
                )

        for function_contexts in self._function_expressions.values():
            for src, function_nodes in function_contexts.items():
                if len(function_nodes) > self.options.max_function_expressions:
                    self.add_violation(
                        complexity.OverusedExpressionViolation(
                            function_nodes[0],
                            text=self._msg.format(src, len(function_nodes)),
                            baseline=self.options.max_function_expressions,
                        ),
                    )

wemake_python_styleguide/visitors/ast/complexity/overuses.py
import ast
from collections import defaultdict
from typing import (
    Callable,
    ClassVar,
    DefaultDict,
    FrozenSet,
    List,
    Tuple,
    Union,
)

from typing_extensions import TypeAlias, final

from wemake_python_styleguide.compat.aliases import FunctionNodes
from wemake_python_styleguide.logic import source, walk
from wemake_python_styleguide.logic.complexity import overuses
from wemake_python_styleguide.logic.tree import annotations
from wemake_python_styleguide.types import AnyNodes, AnyText, AnyTextPrimitive
from wemake_python_styleguide.violations import complexity
from wemake_python_styleguide.visitors import base, decorators

#: We use these types to store the number of nodes usage in different contexts.
_Expressions: TypeAlias = DefaultDict[str, List[ast.AST]]
_FunctionExpressions: TypeAlias = DefaultDict[ast.AST, _Expressions]
_StringConstants: TypeAlias = FrozenSet[Union[str, bytes]]


@final
@decorators.alias('visit_any_string', (
    'visit_Str',
    'visit_Bytes',
))
class StringOveruseVisitor(base.BaseNodeVisitor):
    """
    Restricts repeated usage of the same string constant.

    NB: Some short strings are ignored, as their use is very common and
    forcing assignment would not make much sense (i.e. newlines, "",
    comma, dot).
    """

    _ignored_string_constants: ClassVar[_StringConstants] = frozenset((
        ' ',
        '.',
        ',',
        '',
        '\n',
        '\r\n',
        '\t',
        '|',
        '"',
        "'",
        b'"',
        b"'",
        b' ',
        b'.',
        b',',
        b'',
        b'\n',
        b'\r\n',
        b'\t',
    ))

    def __init__(self, *args, **kwargs) -> None:
        """Inits the counter for constants."""
        super().__init__(*args, **kwargs)
        self._string_constants: DefaultDict[
            AnyTextPrimitive, int,
        ] = defaultdict(int)

    def visit_any_string(self, node: AnyText) -> None:
        """Restricts to over-use string constants."""
        self._check_string_constant(node)
        self.generic_visit(node)

    def _check_string_constant(self, node: AnyText) -> None:
        if annotations.is_annotation(node):
            return

        # Some strings are so common, that it makes no sense to check if
        # they are overused.
        if node.s in self._ignored_string_constants:
            return

        self._string_constants[node.s] += 1

    def _post_visit(self) -> None:
        for string, usage_count in self._string_constants.items():
            if usage_count > self.options.max_string_usages:
                self.add_violation(
                    complexity.OverusedStringViolation(
                        text=source.render_string(string) or "''",
                        baseline=self.options.max_string_usages,
                    ),
                )


@final
class ExpressionOveruseVisitor(base.BaseNodeVisitor):
    """Finds overused expressions."""

    _expressions: ClassVar[AnyNodes] = (
        # We do not treat `ast.Attribute`s as expressions
        # because they are too widely used. That's a compromise.
        ast.Assert,
        ast.BoolOp,
        ast.BinOp,
        ast.UnaryOp,
        ast.Call,
        ast.Compare,
        ast.Subscript,
        ast.Lambda,

        ast.DictComp,
        ast.Dict,
        ast.List,
        ast.ListComp,
        ast.Tuple,
        ast.GeneratorExp,
        ast.Set,
        ast.SetComp,
    )

    _ignore_predicates: Tuple[Callable[[ast.AST], bool], ...] = (
        overuses.is_decorator,
        overuses.is_self,
        annotations.is_annotation,
        overuses.is_class_context,
        overuses.is_super_call,
        overuses.is_primitive,
        overuses.is_unary_minus,
    )

    _msg: ClassVar[str] = '{0}; used {1}'

    def __init__(self, *args, **kwargs) -> None:
        """We need to track expression usage in functions and modules."""
        super().__init__(*args, **kwargs)
        self._module_expressions: _Expressions = defaultdict(list)
        self._function_expressions: _FunctionExpressions = defaultdict(
            lambda: defaultdict(list),
        )

    def visit(self, node: ast.AST) -> None:
        """Visits all nodes in a module to find overused values."""
        if isinstance(node, self._expressions):
            self._add_expression(node)
        self.generic_visit(node)

    def _add_expression(self, node: ast.AST) -> None:
        if any(ignore(node) for ignore in self._ignore_predicates):
            return

        source_code = source.node_to_string(node)
        self._module_expressions[source_code].append(node)

        maybe_function = walk.get_closest_parent(node, FunctionNodes)
        if maybe_function is not None:
            self._function_expressions[maybe_function][source_code].append(
                node,
            )

    def _post_visit(self) -> None:
        for mod_source, module_nodes in self._module_expressions.items():
            if len(module_nodes) > self.options.max_module_expressions:
                self.add_violation(
                    complexity.OverusedExpressionViolation(
                        module_nodes[0],
                        text=self._msg.format(mod_source, len(module_nodes)),
                        baseline=self.options.max_module_expressions,
                    ),
                )

        for function_contexts in self._function_expressions.values():
            for src, function_nodes in function_contexts.items():
                if len(function_nodes) > self.options.max_function_expressions:
                    self.add_violation(
                        complexity.OverusedExpressionViolation(
                            function_nodes[0],
                            text=self._msg.format(src, len(function_nodes)),
                            baseline=self.options.max_function_expressions,
                        ),
                    )

wemake_python_styleguide/visitors/ast/complexity/overuses.py
diff --git a/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py b/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py
index 7fb4dad0f..dfb6190c9 100644
--- a/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py
+++ b/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py
@@ -204,6 +204,8 @@ def test_string_type_annotations(
     '""',
     '","',
     '"."',
+    "'\"'",
+    '"\'"',
 ])
 @pytest.mark.parametrize('prefix', [
     'b',
diff --git a/wemake_python_styleguide/visitors/ast/complexity/overuses.py b/wemake_python_styleguide/visitors/ast/complexity/overuses.py
index 39d25855c..7f755edf5 100644
--- a/wemake_python_styleguide/visitors/ast/complexity/overuses.py
+++ b/wemake_python_styleguide/visitors/ast/complexity/overuses.py
@@ -49,6 +49,10 @@ class StringOveruseVisitor(base.BaseNodeVisitor):
         '\r\n',
         '\t',
         '|',
+        '"',
+        "'",
+        b'"',
+        b"'",
         b' ',
         b'.',
         b',',

False positive WPS226 in f strings
### What's wrong

line
f'query:"{query}"'
in ast node generate node with string value " (double qoute)

so if we have several lines like with we get tricky and false positive violation of WPS226

 WPS226 Found string literal over-use: " > 3


### How it should be

f string should be considered as single node.

### Flake8 version and plugins

{
  "dependencies": [],
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.9.9",
    "system": "Linux"
  },
  "plugins": [
    {
      "is_local": false,
      "plugin": "flake8-bandit",
      "version": "3.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-broken-line",
      "version": "0.4.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-bugbear",
      "version": "22.10.27"
    },
    {
      "is_local": false,
      "plugin": "flake8-comprehensions",
      "version": "3.10.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-darglint",
      "version": "1.8.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-debugger",
      "version": "4.1.2"
    },
    {
      "is_local": false,
      "plugin": "flake8-docstrings",
      "version": "1.6.0, pydocstyle: 6.1.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-eradicate",
      "version": "1.4.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-string-format",
      "version": "0.3.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_commas",
      "version": "2.1.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_isort",
      "version": "4.2.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_quotes",
      "version": "3.3.1"
    },
    {
      "is_local": false,
      "plugin": "mccabe",
      "version": "0.6.1"
    },
    {
      "is_local": false,
      "plugin": "naming",
      "version": "0.12.1"
    },
    {
      "is_local": false,
      "plugin": "pycodestyle",
      "version": "2.8.0"
    },
    {
      "is_local": false,
      "plugin": "pyflakes",
      "version": "2.4.0"
    },
    {
      "is_local": false,
      "plugin": "rst-docstrings",
      "version": "0.2.7"
    },
    {
      "is_local": false,
      "plugin": "wemake_python_styleguide",
      "version": "0.16.1"
    }
  ],
  "version": "4.0.1"
}


### pip information

pip 22.3.1
(python 3.9)
absl-py==1.3.0
aiodns==3.0.0
aiohttp==3.8.1
aiosignal==1.3.1
aniso8601==7.0.0
asgiref==3.5.2
astor==0.8.1
astroid==2.12.12
astunparse==1.6.3
async-timeout==4.0.2
attrs==22.1.0
autoflake==1.4
bandit==1.7.4
black==22.3.0
boto3==1.23.4
botocore==1.26.10
Brotli==1.0.9
cachetools==5.0.0
cchardet==2.1.7
certifi==2022.9.24
cffi==1.15.1
cfgv==3.3.1
charset-normalizer==2.0.12
click==8.1.3
ConfigArgParse==1.5.3
coverage==6.5.0
darglint==1.8.1
Deprecated==1.2.13
dill==0.3.6
distlib==0.3.6
dj-database-url==1.0.0
dj-email-url==1.0.6
Django==4.0.6
django-cache-url==3.4.2
django-cors-headers==3.12.0
django-injector==0.2.5
django-stubs==1.13.0
django-stubs-ext==0.7.0
docutils==0.19
environs==9.5.0
eradicate==2.1.0
filelock==3.8.0
flake8==4.0.1
flake8-bandit==3.0.0
flake8-broken-line==0.4.0
flake8-bugbear==22.10.27
flake8-commas==2.1.0
flake8-comprehensions==3.10.1
flake8-debugger==4.1.2
flake8-docstrings==1.6.0
flake8-eradicate==1.4.0
flake8-isort==4.2.0
flake8-polyfill==1.0.2
flake8-quotes==3.3.1
flake8-rst-docstrings==0.2.7
flake8-string-format==0.3.0
Flask==2.2.2
Flask-BasicAuth==0.2.0
Flask-Cors==3.0.10
flatbuffers==1.12
frozenlist==1.3.3
gast==0.4.0
gevent==22.10.2
geventhttpclient==2.0.8
gitdb==4.0.9
GitPython==3.1.29
google-auth==2.14.1
google-auth-oauthlib==0.4.6
google-pasta==0.2.0
graphene==2.1.9
graphene-django==2.15.0
graphql-core==2.3.2
graphql-relay==2.0.1
greenlet==2.0.1
grpcio==1.50.0
gunicorn==20.1.0
h5py==3.7.0
hash-chunker==0.1.9
identify==2.5.8
idna==3.4
importlib-metadata==5.0.0
inflect==5.5.2
iniconfig==1.1.1
injector==0.20.1
isort==5.10.1
itsdangerous==2.1.2
Jinja2==3.1.2
jmespath==1.0.1
joblib==1.2.0
kazoo==2.8.0
keras==2.9.0
Keras-Preprocessing==1.1.2
lazy-object-proxy==1.8.0
libclang==14.0.6
locust==2.9.0
Markdown==3.4.1
MarkupSafe==2.1.1
marshmallow==3.19.0
mccabe==0.6.1
msgpack==1.0.4
multidict==6.0.2
mypy==0.990
mypy-extensions==0.4.3
mysqlclient==2.1.0
nodeenv==1.7.0
numpy==1.23.4
oauthlib==3.2.2
opt-einsum==3.3.0
packaging==21.3
pandas==1.4.2
pathspec==0.10.2
pbr==5.11.0
pep8-naming==0.12.1
platformdirs==2.5.4
pluggy==0.13.1
pre-commit==2.16.0
promise==2.3
protobuf==3.19.6
psutil==5.9.4
py==1.11.0
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycares==4.2.2
pycodestyle==2.8.0
pycparser==2.21
pydocstyle==6.1.1
pyflakes==2.4.0
Pygments==2.13.0
pylint==2.15.4
pylint-django==2.5.3
pylint-plugin-utils==0.7
PyMySQL==1.0.2
pyparsing==3.0.9
pytest==6.2.4
pytest-cov==2.12.0
pytest-django==4.5.2
pytest-lazy-fixture==0.6.3
python-dateutil==2.8.2
python-dotenv==0.21.0
pytz==2022.6
PyYAML==6.0
pyzmq==22.3.0
redis==4.2.2
requests==2.27.1
requests-oauthlib==1.3.1
restructuredtext-lint==1.4.0
roundrobin==0.0.4
rsa==4.9
Rx==1.6.1
s3transfer==0.5.2
scikit-learn==1.0
scipy==1.9.3
singledispatch==3.7.0
six==1.16.0
smmap==5.0.0
snowballstemmer==2.2.0
SQLAlchemy==1.4.36
sqlparse==0.4.3
stevedore==4.1.1
tenacity==8.0.1
tensorboard==2.9.1
tensorboard-data-server==0.6.1
tensorboard-plugin-wit==1.8.1
tensorflow-cpu==2.9.1
tensorflow-estimator==2.9.0
tensorflow-io-gcs-filesystem==0.27.0
termcolor==2.1.0
text-unidecode==1.3
threadpoolctl==3.1.0
toml==0.10.2
tomli==2.0.1
tomlkit==0.11.6
tqdm==4.64.0
types-cachetools==5.0.1
types-pytz==2022.6.0.1
types-PyYAML==6.0.12.2
types-redis==4.2.5
types-requests==2.27.19
types-urllib3==1.26.25.3
typing_extensions==4.4.0
urllib3==1.26.12
virtualenv==20.16.7
wemake-python-styleguide==0.16.1
Werkzeug==2.2.2
wrapt==1.14.1
yarl==1.8.1
zipp==3.10.0
zope.event==4.5.0
zope.interface==5.5.1


### OS information

Ubuntu 22.04.1 LTS
