feat: refactoring project
This commit is contained in:
21
node_modules/terser/lib/compress/common.js
generated
vendored
21
node_modules/terser/lib/compress/common.js
generated
vendored
@@ -107,6 +107,23 @@ export function make_sequence(orig, expressions) {
|
||||
});
|
||||
}
|
||||
|
||||
export function make_empty_function(self) {
|
||||
return make_node(AST_Function, self, {
|
||||
uses_arguments: false,
|
||||
argnames: [],
|
||||
body: [],
|
||||
is_generator: false,
|
||||
async: false,
|
||||
variables: new Map(),
|
||||
uses_with: false,
|
||||
uses_eval: false,
|
||||
parent_scope: null,
|
||||
enclosed: [],
|
||||
cname: 0,
|
||||
block_scope: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export function make_node_from_constant(val, orig) {
|
||||
switch (typeof val) {
|
||||
case "string":
|
||||
@@ -333,9 +350,9 @@ export function is_reachable(scope_node, defs) {
|
||||
}
|
||||
|
||||
/** Check if a ref refers to the name of a function/class it's defined within */
|
||||
export function is_recursive_ref(compressor, def) {
|
||||
export function is_recursive_ref(tw, def) {
|
||||
var node;
|
||||
for (var i = 0; node = compressor.parent(i); i++) {
|
||||
for (var i = 0; node = tw.parent(i); i++) {
|
||||
if (node instanceof AST_Lambda || node instanceof AST_Class) {
|
||||
var name = node.name;
|
||||
if (name && name.definition() === def) {
|
||||
|
||||
41
node_modules/terser/lib/compress/drop-unused.js
generated
vendored
41
node_modules/terser/lib/compress/drop-unused.js
generated
vendored
@@ -103,8 +103,8 @@ import {
|
||||
is_empty,
|
||||
is_ref_of,
|
||||
can_be_evicted_from_block,
|
||||
requires_sequence_to_maintain_binding,
|
||||
} from "./common.js";
|
||||
import { is_used_in_expression } from "./inference.js";
|
||||
|
||||
const r_keep_assign = /keep_assign/;
|
||||
|
||||
@@ -503,42 +503,3 @@ AST_Scope.DEFMETHOD("drop_unused", function(compressor) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if a node may be used by the expression it's in
|
||||
* void (0, 1, {node}, 2) -> false
|
||||
* console.log(0, {node}) -> true
|
||||
*/
|
||||
function is_used_in_expression(tw) {
|
||||
for (let p = -1, node, parent; node = tw.parent(p), parent = tw.parent(p + 1); p++) {
|
||||
if (parent instanceof AST_Sequence) {
|
||||
const nth_expression = parent.expressions.indexOf(node);
|
||||
if (nth_expression !== parent.expressions.length - 1) {
|
||||
// Detect (0, x.noThis)() constructs
|
||||
const grandparent = tw.parent(p + 2);
|
||||
if (
|
||||
parent.expressions.length > 2
|
||||
|| parent.expressions.length === 1
|
||||
|| !requires_sequence_to_maintain_binding(grandparent, parent, parent.expressions[1])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (parent instanceof AST_Unary) {
|
||||
const op = parent.operator;
|
||||
if (op === "void") {
|
||||
return false;
|
||||
}
|
||||
if (op === "typeof" || op === "+" || op === "-" || op === "!" || op === "~") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
23
node_modules/terser/lib/compress/evaluate.js
generated
vendored
23
node_modules/terser/lib/compress/evaluate.js
generated
vendored
@@ -218,14 +218,25 @@ def_eval(AST_Object, function (compressor, depth) {
|
||||
var non_converting_unary = makePredicate("! typeof void");
|
||||
def_eval(AST_UnaryPrefix, function (compressor, depth) {
|
||||
var e = this.expression;
|
||||
// Function would be evaluated to an array and so typeof would
|
||||
// incorrectly return 'object'. Hence making is a special case.
|
||||
if (compressor.option("typeofs")
|
||||
&& this.operator == "typeof"
|
||||
&& (e instanceof AST_Lambda
|
||||
&& this.operator == "typeof") {
|
||||
// Function would be evaluated to an array and so typeof would
|
||||
// incorrectly return 'object'. Hence making is a special case.
|
||||
if (e instanceof AST_Lambda
|
||||
|| e instanceof AST_SymbolRef
|
||||
&& e.fixed_value() instanceof AST_Lambda)) {
|
||||
return typeof function () { };
|
||||
&& e.fixed_value() instanceof AST_Lambda) {
|
||||
return typeof function () { };
|
||||
}
|
||||
if (
|
||||
(e instanceof AST_Object
|
||||
|| e instanceof AST_Array
|
||||
|| (e instanceof AST_SymbolRef
|
||||
&& (e.fixed_value() instanceof AST_Object
|
||||
|| e.fixed_value() instanceof AST_Array)))
|
||||
&& !e.has_side_effects(compressor)
|
||||
) {
|
||||
return typeof {};
|
||||
}
|
||||
}
|
||||
if (!non_converting_unary.has(this.operator))
|
||||
depth++;
|
||||
|
||||
67
node_modules/terser/lib/compress/index.js
generated
vendored
67
node_modules/terser/lib/compress/index.js
generated
vendored
@@ -176,6 +176,7 @@ import {
|
||||
is_undefined,
|
||||
is_lhs,
|
||||
aborts,
|
||||
is_used_in_expression,
|
||||
} from "./inference.js";
|
||||
import {
|
||||
SQUEEZED,
|
||||
@@ -195,6 +196,7 @@ import {
|
||||
make_sequence,
|
||||
best_of,
|
||||
best_of_expression,
|
||||
make_empty_function,
|
||||
make_node_from_constant,
|
||||
merge_sequence,
|
||||
get_simple_key,
|
||||
@@ -510,9 +512,8 @@ def_optimize(AST_Node, function(self) {
|
||||
});
|
||||
|
||||
AST_Toplevel.DEFMETHOD("drop_console", function(options) {
|
||||
var isArray = Array.isArray(options);
|
||||
|
||||
return this.transform(new TreeTransformer(function(self) {
|
||||
const isArray = Array.isArray(options);
|
||||
const tt = new TreeTransformer(function(self) {
|
||||
if (self.TYPE !== "Call") {
|
||||
return;
|
||||
}
|
||||
@@ -523,18 +524,35 @@ AST_Toplevel.DEFMETHOD("drop_console", function(options) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArray && options.indexOf(exp.property) === -1) {
|
||||
if (isArray && !options.includes(exp.property)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var name = exp.expression;
|
||||
var depth = 2;
|
||||
while (name.expression) {
|
||||
name = name.expression;
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (is_undeclared_ref(name) && name.name == "console") {
|
||||
return make_node(AST_Undefined, self);
|
||||
if (
|
||||
depth === 3
|
||||
&& !["call", "apply"].includes(exp.property)
|
||||
&& is_used_in_expression(tt)
|
||||
) {
|
||||
// a (used) call to Function.prototype methods (eg: console.log.bind(console))
|
||||
// but not .call and .apply which would also return undefined.
|
||||
exp.expression = make_empty_function(self);
|
||||
set_flag(exp.expression, SQUEEZED);
|
||||
self.args = [];
|
||||
} else {
|
||||
return make_node(AST_Undefined, self);
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return this.transform(tt);
|
||||
});
|
||||
|
||||
AST_Node.DEFMETHOD("equivalent_to", function(node) {
|
||||
@@ -1233,7 +1251,9 @@ def_optimize(AST_Switch, function(self, compressor) {
|
||||
eliminate_branch(branch, body[body.length - 1]);
|
||||
continue;
|
||||
}
|
||||
if (exp instanceof AST_Node) exp = branch.expression.tail_node().evaluate(compressor);
|
||||
if (exp instanceof AST_Node && !exp.has_side_effects(compressor)) {
|
||||
exp = branch.expression.tail_node().evaluate(compressor);
|
||||
}
|
||||
if (exp === value) {
|
||||
exact_match = branch;
|
||||
if (default_branch) {
|
||||
@@ -1415,12 +1435,9 @@ def_optimize(AST_Switch, function(self, compressor) {
|
||||
break DEFAULT;
|
||||
}
|
||||
|
||||
let sideEffect = body.find(branch => {
|
||||
return (
|
||||
branch !== default_or_exact
|
||||
&& branch.expression.has_side_effects(compressor)
|
||||
);
|
||||
});
|
||||
let sideEffect = body.find(
|
||||
branch => branch !== default_or_exact && branch.expression.has_side_effects(compressor)
|
||||
);
|
||||
// If no cases cause a side-effect, we can eliminate the switch entirely.
|
||||
if (!sideEffect) {
|
||||
return make_node(AST_BlockStatement, self, {
|
||||
@@ -1528,9 +1545,10 @@ def_optimize(AST_Switch, function(self, compressor) {
|
||||
right: branch.expression,
|
||||
}),
|
||||
body: consequent,
|
||||
alternative: null
|
||||
})
|
||||
].concat(always)
|
||||
alternative: null,
|
||||
}),
|
||||
always,
|
||||
],
|
||||
}).optimize(compressor);
|
||||
}
|
||||
return self;
|
||||
@@ -1553,13 +1571,12 @@ def_optimize(AST_Switch, function(self, compressor) {
|
||||
let pblock = make_node(AST_BlockStatement, prev, { body: pbody });
|
||||
return bblock.equivalent_to(pblock);
|
||||
}
|
||||
function statement(expression) {
|
||||
return make_node(AST_SimpleStatement, expression, {
|
||||
body: expression
|
||||
});
|
||||
function statement(body) {
|
||||
return make_node(AST_SimpleStatement, body, { body });
|
||||
}
|
||||
function has_nested_break(root) {
|
||||
let has_break = false;
|
||||
|
||||
let tw = new TreeWalker(node => {
|
||||
if (has_break) return true;
|
||||
if (node instanceof AST_Lambda) return true;
|
||||
@@ -1912,10 +1929,7 @@ def_optimize(AST_Call, function(self, compressor) {
|
||||
&& is_undeclared_ref(exp)
|
||||
&& exp.name == "Function") {
|
||||
// new Function() => function(){}
|
||||
if (self.args.length == 0) return make_node(AST_Function, self, {
|
||||
argnames: [],
|
||||
body: []
|
||||
}).optimize(compressor);
|
||||
if (self.args.length == 0) return make_empty_function(self).optimize(compressor);
|
||||
if (self.args.every((x) => x instanceof AST_String)) {
|
||||
// quite a corner-case, but we can handle it:
|
||||
// https://github.com/mishoo/UglifyJS2/issues/203
|
||||
@@ -3675,10 +3689,7 @@ def_optimize(AST_Dot, function(self, compressor) {
|
||||
});
|
||||
break;
|
||||
case "Function":
|
||||
self.expression = make_node(AST_Function, self.expression, {
|
||||
argnames: [],
|
||||
body: []
|
||||
});
|
||||
self.expression = make_empty_function(self.expression);
|
||||
break;
|
||||
case "Number":
|
||||
self.expression = make_node(AST_Number, self.expression, {
|
||||
|
||||
52
node_modules/terser/lib/compress/inference.js
generated
vendored
52
node_modules/terser/lib/compress/inference.js
generated
vendored
@@ -121,7 +121,7 @@ import {
|
||||
member,
|
||||
has_annotation,
|
||||
} from "../utils/index.js";
|
||||
import { make_sequence, best_of_expression, read_property } from "./common.js";
|
||||
import { make_sequence, best_of_expression, read_property, requires_sequence_to_maintain_binding } from "./common.js";
|
||||
|
||||
import { INLINED, UNDEFINED, has_flag } from "./compressor-flags.js";
|
||||
import { pure_prop_access_globals, is_pure_native_fn, is_pure_native_method } from "./native-objects.js";
|
||||
@@ -424,7 +424,7 @@ export function is_nullish(node, compressor) {
|
||||
return any(this.definitions, compressor);
|
||||
});
|
||||
def_has_side_effects(AST_VarDef, function() {
|
||||
return this.value;
|
||||
return this.value != null;
|
||||
});
|
||||
def_has_side_effects(AST_TemplateSegment, return_false);
|
||||
def_has_side_effects(AST_TemplateString, function(compressor) {
|
||||
@@ -982,3 +982,51 @@ export function is_modified(compressor, tw, node, value, level, immutable) {
|
||||
return !immutable && is_modified(compressor, tw, parent, prop, level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node may be used by the expression it's in
|
||||
* void (0, 1, {node}, 2) -> false
|
||||
* console.log(0, {node}) -> true
|
||||
*/
|
||||
export function is_used_in_expression(tw) {
|
||||
for (let p = -1, node, parent; node = tw.parent(p), parent = tw.parent(p + 1); p++) {
|
||||
if (parent instanceof AST_Sequence) {
|
||||
const nth_expression = parent.expressions.indexOf(node);
|
||||
if (nth_expression !== parent.expressions.length - 1) {
|
||||
// Detect (0, x.noThis)() constructs
|
||||
const grandparent = tw.parent(p + 2);
|
||||
if (
|
||||
parent.expressions.length > 2
|
||||
|| parent.expressions.length === 1
|
||||
|| !requires_sequence_to_maintain_binding(grandparent, parent, parent.expressions[1])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (parent instanceof AST_Unary) {
|
||||
const op = parent.operator;
|
||||
if (op === "void") {
|
||||
return false;
|
||||
}
|
||||
if (op === "typeof" || op === "+" || op === "-" || op === "!" || op === "~") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (
|
||||
parent instanceof AST_SimpleStatement
|
||||
|| parent instanceof AST_LabeledStatement
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (parent instanceof AST_Scope) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
177
node_modules/terser/lib/compress/reduce-vars.js
generated
vendored
177
node_modules/terser/lib/compress/reduce-vars.js
generated
vendored
@@ -94,8 +94,7 @@ import {
|
||||
|
||||
walk,
|
||||
walk_body,
|
||||
|
||||
TreeWalker,
|
||||
walk_parent,
|
||||
} from "../ast.js";
|
||||
import { HOP, make_node, noop } from "../utils/index.js";
|
||||
|
||||
@@ -496,28 +495,50 @@ function mark_lambda(tw, descend, compressor) {
|
||||
* // use defined_after
|
||||
* }
|
||||
*
|
||||
* This function is called on the parent to handle this issue.
|
||||
* Or even indirectly:
|
||||
*
|
||||
* B();
|
||||
* var defined_after = true;
|
||||
* function A() {
|
||||
* // use defined_after
|
||||
* }
|
||||
* function B() {
|
||||
* A();
|
||||
* }
|
||||
*
|
||||
* Access a variable before declaration will either throw a ReferenceError
|
||||
* (if the variable is declared with `let` or `const`),
|
||||
* or get an `undefined` (if the variable is declared with `var`).
|
||||
*
|
||||
* If the variable is inlined into the function, the behavior will change.
|
||||
*
|
||||
* This function is called on the parent to disallow inlining of such variables,
|
||||
*/
|
||||
function handle_defined_after_hoist(parent) {
|
||||
const defuns = [];
|
||||
walk(parent, node => {
|
||||
if (node === parent) return;
|
||||
if (node instanceof AST_Defun) defuns.push(node);
|
||||
if (node instanceof AST_Defun) {
|
||||
defuns.push(node);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
node instanceof AST_Scope
|
||||
|| node instanceof AST_SimpleStatement
|
||||
) return true;
|
||||
});
|
||||
|
||||
// `defun` id to array of `defun` it uses
|
||||
const defun_dependencies_map = new Map();
|
||||
// `defun` id to array of enclosing `def` that are used by the function
|
||||
const dependencies_map = new Map();
|
||||
// all symbol ids that will be tracked for read/write
|
||||
const symbols_of_interest = new Set();
|
||||
const defuns_of_interest = new Set();
|
||||
const potential_conflicts = [];
|
||||
|
||||
for (const defun of defuns) {
|
||||
const fname_def = defun.name.definition();
|
||||
const found_self_ref_in_other_defuns = defuns.some(
|
||||
d => d !== defun && d.enclosed.indexOf(fname_def) !== -1
|
||||
);
|
||||
const enclosing_defs = [];
|
||||
|
||||
for (const def of defun.enclosed) {
|
||||
if (
|
||||
@@ -528,93 +549,107 @@ function handle_defined_after_hoist(parent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// defun is hoisted, so always safe
|
||||
symbols_of_interest.add(def.id);
|
||||
|
||||
// found a reference to another function
|
||||
if (
|
||||
def.assignments === 0
|
||||
&& def.orig.length === 1
|
||||
&& def.orig[0] instanceof AST_SymbolDefun
|
||||
) {
|
||||
defuns_of_interest.add(def.id);
|
||||
symbols_of_interest.add(def.id);
|
||||
|
||||
defuns_of_interest.add(fname_def.id);
|
||||
symbols_of_interest.add(fname_def.id);
|
||||
|
||||
if (!defun_dependencies_map.has(fname_def.id)) {
|
||||
defun_dependencies_map.set(fname_def.id, []);
|
||||
}
|
||||
defun_dependencies_map.get(fname_def.id).push(def.id);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (found_self_ref_in_other_defuns) {
|
||||
def.fixed = false;
|
||||
continue;
|
||||
}
|
||||
enclosing_defs.push(def);
|
||||
}
|
||||
|
||||
// for the slower checks below this loop
|
||||
potential_conflicts.push({ defun, def, fname_def });
|
||||
symbols_of_interest.add(def.id);
|
||||
if (enclosing_defs.length) {
|
||||
dependencies_map.set(fname_def.id, enclosing_defs);
|
||||
defuns_of_interest.add(fname_def.id);
|
||||
symbols_of_interest.add(fname_def.id);
|
||||
defuns_of_interest.add(defun);
|
||||
}
|
||||
}
|
||||
|
||||
// linearize all symbols, and locate defs that are read after the defun
|
||||
if (potential_conflicts.length) {
|
||||
// All "symbols of interest", that is, defuns or defs, that we found.
|
||||
// These are placed in order so we can check which is after which.
|
||||
const found_symbols = [];
|
||||
// Indices of `found_symbols` which are writes
|
||||
const found_symbol_writes = new Set();
|
||||
// Defun ranges are recorded because we don't care if a function uses the def internally
|
||||
const defun_ranges = new Map();
|
||||
// No defuns use outside constants
|
||||
if (!dependencies_map.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tw;
|
||||
parent.walk((tw = new TreeWalker((node, descend) => {
|
||||
if (node instanceof AST_Defun && defuns_of_interest.has(node)) {
|
||||
const start = found_symbols.length;
|
||||
descend();
|
||||
const end = found_symbols.length;
|
||||
// Increment to count "symbols of interest" (defuns or defs) that we found.
|
||||
// These are tracked in AST order so we can check which is after which.
|
||||
let symbol_index = 1;
|
||||
// Map a defun ID to its first read (a `symbol_index`)
|
||||
const defun_first_read_map = new Map();
|
||||
// Map a symbol ID to its last write (a `symbol_index`)
|
||||
const symbol_last_write_map = new Map();
|
||||
|
||||
defun_ranges.set(node, { start, end });
|
||||
return true;
|
||||
}
|
||||
// if we found a defun on the list, mark IN_DEFUN=id and descend
|
||||
walk_parent(parent, (node, walk_info) => {
|
||||
if (node instanceof AST_Symbol && node.thedef) {
|
||||
const id = node.definition().id;
|
||||
|
||||
if (node instanceof AST_Symbol && node.thedef) {
|
||||
const id = node.definition().id;
|
||||
if (symbols_of_interest.has(id)) {
|
||||
if (node instanceof AST_SymbolDeclaration || is_lhs(node, tw)) {
|
||||
found_symbol_writes.add(found_symbols.length);
|
||||
}
|
||||
found_symbols.push(id);
|
||||
symbol_index++;
|
||||
|
||||
// Track last-writes to symbols
|
||||
if (symbols_of_interest.has(id)) {
|
||||
if (node instanceof AST_SymbolDeclaration || is_lhs(node, walk_info.parent())) {
|
||||
symbol_last_write_map.set(id, symbol_index);
|
||||
}
|
||||
}
|
||||
})));
|
||||
|
||||
for (const { def, defun, fname_def } of potential_conflicts) {
|
||||
const defun_range = defun_ranges.get(defun);
|
||||
|
||||
// find the index in `found_symbols`, with some special rules:
|
||||
const find = (sym_id, starting_at = 0, must_be_write = false) => {
|
||||
let index = starting_at;
|
||||
|
||||
for (;;) {
|
||||
index = found_symbols.indexOf(sym_id, index);
|
||||
|
||||
if (index === -1) {
|
||||
break;
|
||||
} else if (index >= defun_range.start && index < defun_range.end) {
|
||||
index = defun_range.end;
|
||||
continue;
|
||||
} else if (must_be_write && !found_symbol_writes.has(index)) {
|
||||
index++;
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
// Track first-reads of defuns (refined later)
|
||||
if (defuns_of_interest.has(id)) {
|
||||
if (!defun_first_read_map.has(id) && !is_recursive_ref(walk_info, id)) {
|
||||
defun_first_read_map.set(id, symbol_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return index;
|
||||
};
|
||||
// Refine `defun_first_read_map` to be as high as possible
|
||||
for (const [defun, defun_first_read] of defun_first_read_map) {
|
||||
// Update all depdencies of `defun`
|
||||
const queue = new Set(defun_dependencies_map.get(defun));
|
||||
for (const enclosed_defun of queue) {
|
||||
let enclosed_defun_first_read = defun_first_read_map.get(enclosed_defun);
|
||||
if (enclosed_defun_first_read != null && enclosed_defun_first_read < defun_first_read) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const read_defun_at = find(fname_def.id);
|
||||
const wrote_def_at = find(def.id, read_defun_at + 1, true);
|
||||
defun_first_read_map.set(enclosed_defun, defun_first_read);
|
||||
|
||||
const wrote_def_after_reading_defun = read_defun_at != -1 && wrote_def_at != -1 && wrote_def_at > read_defun_at;
|
||||
for (const enclosed_enclosed_defun of defun_dependencies_map.get(enclosed_defun) || []) {
|
||||
queue.add(enclosed_enclosed_defun);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wrote_def_after_reading_defun) {
|
||||
// ensure write-then-read order, otherwise clear `fixed`
|
||||
// This is safe because last-writes (found_symbol_writes) are assumed to be as late as possible, and first-reads (defun_first_read_map) are assumed to be as early as possible.
|
||||
for (const [defun, defs] of dependencies_map) {
|
||||
const defun_first_read = defun_first_read_map.get(defun);
|
||||
if (defun_first_read === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const def of defs) {
|
||||
if (def.fixed === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let def_last_write = symbol_last_write_map.get(def.id) || 0;
|
||||
|
||||
if (defun_first_read < def_last_write) {
|
||||
def.fixed = false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user