|
20 | 20 | } |
21 | 21 | ) |
22 | 22 |
|
| 23 | +PLOT_COUNT_LIMIT = 64 |
| 24 | + |
23 | 25 |
|
24 | 26 | @dataclass(slots=True) |
25 | 27 | class Symbol: |
@@ -71,11 +73,15 @@ def __init__(self) -> None: |
71 | 73 | self.errors: list[Diagnostic] = [] |
72 | 74 | self.function_declarations: dict[str, AST.FunctionDeclaration] = {} |
73 | 75 | self.consistency_sensitive_functions: set[str] = set(CONSISTENCY_SENSITIVE_BUILTINS) |
| 76 | + self.plot_count = 0 |
| 77 | + self.plot_count_reported = False |
74 | 78 |
|
75 | 79 | def validate(self, program: AST.Program) -> list[Diagnostic]: |
76 | 80 | self.errors = [] |
77 | 81 | self.function_declarations = {} |
78 | 82 | self.consistency_sensitive_functions = set(CONSISTENCY_SENSITIVE_BUILTINS) |
| 83 | + self.plot_count = 0 |
| 84 | + self.plot_count_reported = False |
79 | 85 | self.collect_function_declarations(program.body) |
80 | 86 | self.mark_consistency_sensitive_functions() |
81 | 87 | global_scope = Scope() |
@@ -418,6 +424,7 @@ def validate_expression(self, expression: AST.Expression, scope: Scope, conditio |
418 | 424 | if spec is not None: |
419 | 425 | self.validate_call_signature(expression, resolved_function_name, spec) |
420 | 426 | self.validate_special_cases(expression, resolved_function_name) |
| 427 | + self.track_plot_count(expression, resolved_function_name, scope) |
421 | 428 | return |
422 | 429 |
|
423 | 430 | if isinstance(expression, AST.BinaryExpression): |
@@ -860,6 +867,129 @@ def validate_line_new_positional_types(self, call: AST.CallExpression) -> None: |
860 | 867 | ) |
861 | 868 | ) |
862 | 869 |
|
| 870 | + def track_plot_count(self, call: AST.CallExpression, function_name: str, scope: Scope) -> None: |
| 871 | + plot_count = self.estimate_plot_count(call, function_name, scope) |
| 872 | + if plot_count <= 0: |
| 873 | + return |
| 874 | + |
| 875 | + self.plot_count += plot_count |
| 876 | + if self.plot_count_reported or self.plot_count <= PLOT_COUNT_LIMIT: |
| 877 | + return |
| 878 | + |
| 879 | + self.plot_count_reported = True |
| 880 | + self.errors.append( |
| 881 | + Diagnostic( |
| 882 | + line=call.line, |
| 883 | + column=call.column, |
| 884 | + length=len(function_name), |
| 885 | + message=( |
| 886 | + f'Estimated plot count is {self.plot_count}, which exceeds the Pine Script limit of {PLOT_COUNT_LIMIT}. ' |
| 887 | + "Reduce plot-producing calls or simplify dynamic color usage." |
| 888 | + ), |
| 889 | + severity=Severity.ERROR, |
| 890 | + source="ast", |
| 891 | + ) |
| 892 | + ) |
| 893 | + |
| 894 | + def estimate_plot_count(self, call: AST.CallExpression, function_name: str, scope: Scope) -> int: |
| 895 | + if function_name == "plot": |
| 896 | + return 1 + self.dynamic_plot_argument_count(call, scope, ("color",)) |
| 897 | + if function_name == "plotarrow": |
| 898 | + return 1 + self.dynamic_plot_argument_count(call, scope, ("colorup", "colordown")) |
| 899 | + if function_name == "plotbar": |
| 900 | + return 4 + self.dynamic_plot_argument_count(call, scope, ("color",)) |
| 901 | + if function_name == "plotcandle": |
| 902 | + return 4 + self.dynamic_plot_argument_count(call, scope, ("color", "wickcolor", "bordercolor")) |
| 903 | + if function_name in {"plotchar", "plotshape"}: |
| 904 | + return 1 + self.dynamic_plot_argument_count(call, scope, ("color", "textcolor")) |
| 905 | + if function_name in {"alertcondition", "bgcolor", "barcolor"}: |
| 906 | + return 1 |
| 907 | + if function_name == "fill": |
| 908 | + color_argument = self.get_argument_value(call, "color", 2) |
| 909 | + return 1 if color_argument is not None and not self.is_const_plot_expression(color_argument, scope) else 0 |
| 910 | + return 0 |
| 911 | + |
| 912 | + def dynamic_plot_argument_count( |
| 913 | + self, |
| 914 | + call: AST.CallExpression, |
| 915 | + scope: Scope, |
| 916 | + parameter_names: tuple[str, ...], |
| 917 | + ) -> int: |
| 918 | + return sum( |
| 919 | + 1 |
| 920 | + for index, parameter_name in enumerate(parameter_names) |
| 921 | + if ( |
| 922 | + argument := self.get_argument_value(call, parameter_name, index + 1) |
| 923 | + ) is not None |
| 924 | + and not self.is_const_plot_expression(argument, scope) |
| 925 | + ) |
| 926 | + |
| 927 | + def get_argument_value(self, call: AST.CallExpression, name: str, positional_index: int) -> AST.Expression | None: |
| 928 | + for argument in call.arguments: |
| 929 | + if argument.name == name: |
| 930 | + return argument.value |
| 931 | + |
| 932 | + positional_arguments = [argument.value for argument in call.arguments if argument.name is None] |
| 933 | + if positional_index < len(positional_arguments): |
| 934 | + return positional_arguments[positional_index] |
| 935 | + return None |
| 936 | + |
| 937 | + def is_const_plot_expression(self, expression: AST.Expression, scope: Scope) -> bool: |
| 938 | + if isinstance(expression, AST.Literal): |
| 939 | + return True |
| 940 | + |
| 941 | + if isinstance(expression, AST.Identifier): |
| 942 | + symbol = self.lookup_accessible_symbol(scope, expression.name, expression.line, expression.column) |
| 943 | + if symbol is not None and symbol.kind == "variable": |
| 944 | + return False |
| 945 | + return expression.name in self.builtins.standalone_variables |
| 946 | + |
| 947 | + if isinstance(expression, AST.MemberExpression): |
| 948 | + return isinstance(expression.object, AST.Identifier) and expression.object.name == "color" |
| 949 | + |
| 950 | + if isinstance(expression, AST.UnaryExpression): |
| 951 | + return self.is_const_plot_expression(expression.argument, scope) |
| 952 | + |
| 953 | + if isinstance(expression, AST.BinaryExpression): |
| 954 | + return self.is_const_plot_expression(expression.left, scope) and self.is_const_plot_expression(expression.right, scope) |
| 955 | + |
| 956 | + if isinstance(expression, AST.TernaryExpression): |
| 957 | + return ( |
| 958 | + self.is_const_plot_expression(expression.condition, scope) |
| 959 | + and self.is_const_plot_expression(expression.consequent, scope) |
| 960 | + and self.is_const_plot_expression(expression.alternate, scope) |
| 961 | + ) |
| 962 | + |
| 963 | + if isinstance(expression, AST.IfExpression): |
| 964 | + return ( |
| 965 | + self.is_const_plot_expression(expression.condition, scope) |
| 966 | + and self.is_const_plot_expression(expression.consequent, scope) |
| 967 | + and self.is_const_plot_expression(expression.alternate, scope) |
| 968 | + ) |
| 969 | + |
| 970 | + if isinstance(expression, AST.CallExpression): |
| 971 | + function_name = self.extract_function_name(expression.callee) |
| 972 | + if function_name in {"color.new", "color.rgb"}: |
| 973 | + return all(self.is_const_plot_expression(argument.value, scope) for argument in expression.arguments) |
| 974 | + return False |
| 975 | + |
| 976 | + if isinstance(expression, AST.ArrayExpression): |
| 977 | + return all(self.is_const_plot_expression(element, scope) for element in expression.elements) |
| 978 | + |
| 979 | + if isinstance(expression, AST.IndexExpression): |
| 980 | + return self.is_const_plot_expression(expression.object, scope) and self.is_const_plot_expression(expression.index, scope) |
| 981 | + |
| 982 | + if isinstance(expression, AST.SwitchExpression): |
| 983 | + if expression.expression is not None and not self.is_const_plot_expression(expression.expression, scope): |
| 984 | + return False |
| 985 | + return all( |
| 986 | + (case.condition is None or self.is_const_plot_expression(case.condition, scope)) |
| 987 | + and self.is_const_plot_expression(case.value, scope) |
| 988 | + for case in expression.cases |
| 989 | + ) |
| 990 | + |
| 991 | + return False |
| 992 | + |
863 | 993 | def describe_argument_type(self, expression: AST.Expression) -> tuple[str | None, str, str]: |
864 | 994 | if isinstance(expression, AST.Literal): |
865 | 995 | if isinstance(expression.value, bool): |
|
0 commit comments