Remove Language from RuleSet class. Previous changes which associated Language with Rule are now the driver for processing instead of the RuleSet.
Work still needs to be done to clean up the XML Schema and DTD. Nothing too complex there. Slightly more complicated work needs to be done allow PMD to appropriately associate an source file based LanguageVersion on the RuleContext. Once that is done, it will be technically possible to use a single RuleSet containing Rules for different Languages and actually run it against a mixed set of source files. git-svn-id: https://pmd.svn.sourceforge.net/svnroot/pmd/trunk@6546 51baf565-9d33-0410-a72c-fc3788e3496d
This commit is contained in:
1 parent
0481bba385
commit
e6eac94c0b
15 files changed
+85
-69
No files matched your search
@@ -4,8 +4,6 @@
|
||||
o SimpleNode.dump() needs to be implemented as a Visitor
|
||||
|
||||
TODO - Release blockers - Must implement before this release can be finished
|
||||
o Change RuleSets to not specify Language. Rule's have already been modified
|
||||
to specify language.
|
||||
o Additional changes to Rule organization withing RuleSets as discussed on
|
||||
this forum thread:
|
||||
http://sourceforge.net/forum/forum.php?thread_id=1323593&forum_id=188194
|
||||
@@ -268,6 +266,9 @@ The following is relatively complete list of the major changes (this may not be
|
||||
Removed - Rule.addProperties(Properties)
|
||||
Removed - boolean Rule.hasProperty(String)
|
||||
Removed - RuleSet.applies(Language,Language)
|
||||
Removed - RuleSet.getLanguage()
|
||||
Removed - RuleSet.setLanguage(Language)
|
||||
Removed - RuleSets.applies(Language,Language)
|
||||
Changed - void Rule.setPriority(int) to void Rule.setPriority(RulePriority)
|
||||
Changed - int Rule.getPriority() to void RulePriority Rule.getPriority()
|
||||
Changed - XXX Rule.getXXXProperty(String) to <T> Rule.getProperty(PropertyDescriptor<T>)
|
||||
@@ -277,6 +278,8 @@ The following is relatively complete list of the major changes (this may not be
|
||||
Changed - Rule.setProperty(PropertyDescriptor, Object[]) to Rule.setProperty(PropertyDescriptor<T>, T)
|
||||
Changed - Rule.propertyValuesByDescriptor() to Rule.getPropertiesByPropertyDescriptor()
|
||||
Changed - PropertyDescriptor Rule.propertyDescriptorFor(String) to PropertyDescriptor Rule.getPropertyDescriptor(String)
|
||||
Changed - boolean RuleSet.usesDFA() to boolean RuleSet.usesDFA(Language)
|
||||
Changed - boolean RuleSet.usesTypeResolution() to boolean RuleSet.usesTypeResolution(Language)
|
||||
Added - Rule.setLanguage(Language)
|
||||
Added - Language Rule.getLanguage()
|
||||
Added - Rule.setMinimumLanguageVersion(LanguageVersion)
|
||||
@@ -287,6 +290,7 @@ The following is relatively complete list of the major changes (this may not be
|
||||
Added - boolean Rule.isDeprecated()
|
||||
Added - Rule.definePropertyDescriptor(PropertyDescriptor)
|
||||
Added - List<PropertyDescriptor> Rule.getPropertyDescriptors()
|
||||
Added - RuleSet.applies(Rule,LanguageVersion)
|
||||
|
||||
API Change - Changes to PMD class
|
||||
Renamed - PMD.EXCLUDE_MARKER to PMD.SUPPRESS_MARKER
|
||||
|
||||
@@ -45,7 +45,6 @@ import test.net.sourceforge.pmd.testframework.TestDescriptor;
|
||||
ctx.setLanguageVersion(DEFAULT_LANGUAGE_VERSION);
|
||||
RuleSet rules = new RuleSet();
|
||||
rules.addRule(rule);
|
||||
rules.setLanguage(DEFAULT_LANGUAGE);
|
||||
p.processFile(new StringReader(TEST3), new RuleSets(rules), ctx);
|
||||
assertTrue(r.isEmpty());
|
||||
assertEquals(r.getSuppressedRuleViolations().size(), 1);
|
||||
|
||||
@@ -310,10 +310,7 @@ public class RuleSetFactoryTest {
|
||||
for (String fileName : ruleSetFileNames) {
|
||||
RuleSet ruleSet = loadRuleSetByFileName(fileName);
|
||||
for (Rule rule : ruleSet.getRules()) {
|
||||
Language language = ruleSet.getLanguage();
|
||||
if (language == null) {
|
||||
language = Language.JAVA;
|
||||
}
|
||||
Language language = Language.JAVA;
|
||||
String group = fileName.substring(fileName.indexOf('/') + 1);
|
||||
group = group.substring(0, group.indexOf(".xml"));
|
||||
if (group.indexOf('-') >= 0) {
|
||||
@@ -485,7 +482,6 @@ public class RuleSetFactoryTest {
|
||||
private void assertEqualsRuleSet(String message, RuleSet ruleSet1, RuleSet ruleSet2) {
|
||||
assertEquals(message + ", RuleSet name", ruleSet1.getName(), ruleSet2.getName());
|
||||
assertEquals(message + ", RuleSet description", ruleSet1.getDescription(), ruleSet2.getDescription());
|
||||
assertEquals(message + ", RuleSet language", ruleSet1.getLanguage(), ruleSet2.getLanguage());
|
||||
assertEquals(message + ", RuleSet exclude patterns", ruleSet1.getExcludePatterns(), ruleSet2
|
||||
.getExcludePatterns());
|
||||
assertEquals(message + ", RuleSet include patterns", ruleSet1.getIncludePatterns(), ruleSet2
|
||||
|
||||
@@ -27,6 +27,7 @@ import net.sourceforge.pmd.RuleSet;
|
||||
import net.sourceforge.pmd.RuleSets;
|
||||
import net.sourceforge.pmd.RuleViolation;
|
||||
import net.sourceforge.pmd.lang.Language;
|
||||
import net.sourceforge.pmd.lang.LanguageVersion;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
|
||||
import net.sourceforge.pmd.lang.rule.MockRule;
|
||||
import net.sourceforge.pmd.lang.rule.RuleReference;
|
||||
@@ -44,7 +45,7 @@ public class RuleSetTest extends RuleTst {
|
||||
RuleSet rs = new RuleSet();
|
||||
MockRule mock = new MockRule("name", "desc", "msg", "rulesetname");
|
||||
rs.addRule(mock);
|
||||
assertFalse(rs.usesDFA());
|
||||
assertFalse(rs.usesDFA(Language.JAVA));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -53,7 +54,7 @@ public class RuleSetTest extends RuleTst {
|
||||
MockRule mock = new MockRule("name", "desc", "msg", "rulesetname");
|
||||
mock.setUsesDFA();
|
||||
rs.addRule(mock);
|
||||
assertTrue(rs.usesDFA());
|
||||
assertTrue(rs.usesDFA(Language.JAVA));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -219,6 +220,26 @@ public class RuleSetTest extends RuleTst {
|
||||
|
||||
assertFalse("2 rulesets with same name but different rules must not be equals", s1.equals(s2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLanguageApplies() {
|
||||
RuleSet ruleSet = new RuleSet();
|
||||
Rule rule = new MockRule();
|
||||
|
||||
rule.setLanguage(Language.EMCASCRIPT);
|
||||
assertFalse("Different languages should not apply", ruleSet.applies(rule, LanguageVersion.JAVA_15));
|
||||
|
||||
rule.setLanguage(Language.JAVA);
|
||||
assertTrue("Same language with no min/max should apply", ruleSet.applies(rule, LanguageVersion.JAVA_15));
|
||||
|
||||
rule.setMinimumLanguageVersion(LanguageVersion.JAVA_15);
|
||||
assertTrue("Same language with valid min only should apply", ruleSet.applies(rule, LanguageVersion.JAVA_15));
|
||||
|
||||
rule.setMaximumLanguageVersion(LanguageVersion.JAVA_16);
|
||||
assertTrue("Same language with valid min and max should apply", ruleSet.applies(rule, LanguageVersion.JAVA_15));
|
||||
assertFalse("Same language with outside range of min/max should not apply", ruleSet.applies(rule, LanguageVersion.JAVA_14));
|
||||
assertFalse("Same language with outside range of min/max should not apply", ruleSet.applies(rule, LanguageVersion.JAVA_17));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddExcludePattern() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package test.net.sourceforge.pmd.jaxen;
|
||||
|
||||
import net.sourceforge.pmd.Rule;
|
||||
import net.sourceforge.pmd.lang.Language;
|
||||
import net.sourceforge.pmd.lang.rule.XPathRule;
|
||||
|
||||
import org.junit.Test;
|
||||
@@ -13,7 +14,7 @@ public class RegexpAcceptanceTest extends SimpleAggregatorTst {
|
||||
@Test
|
||||
public void testSimple() throws Throwable {
|
||||
Rule r = new XPathRule();
|
||||
// r.addProperty("xpath", "//ClassOrInterfaceDeclaration[matches(@Image, 'F?o')]");
|
||||
r.setLanguage(Language.JAVA);
|
||||
r.setProperty(XPathRule.XPATH_DESCRIPTOR, "//ClassOrInterfaceDeclaration[matches(@Image, 'F?o')]");
|
||||
r.setMessage("");
|
||||
runTests(r, "RegexpAcceptance");
|
||||
|
||||
@@ -11,6 +11,7 @@ import net.sourceforge.pmd.RuleContext;
|
||||
import net.sourceforge.pmd.RuleSet;
|
||||
import net.sourceforge.pmd.RuleSets;
|
||||
import net.sourceforge.pmd.RuleViolation;
|
||||
import net.sourceforge.pmd.lang.Language;
|
||||
import net.sourceforge.pmd.lang.rule.XPathRule;
|
||||
import net.sourceforge.pmd.lang.rule.properties.StringProperty;
|
||||
|
||||
@@ -28,12 +29,12 @@ import test.net.sourceforge.pmd.testframework.RuleTst;
|
||||
@Before
|
||||
public void setUp() {
|
||||
rule = new XPathRule();
|
||||
rule.setLanguage(Language.JAVA);
|
||||
rule.setMessage("XPath Rule Failed");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPluginname() throws Throwable {
|
||||
Rule rule = new XPathRule();
|
||||
rule.setProperty(XPathRule.XPATH_DESCRIPTOR, "//VariableDeclaratorId[string-length(@Image) < 3]");
|
||||
rule.setMessage("{0}");
|
||||
PMD p = new PMD();
|
||||
@@ -50,7 +51,6 @@ import test.net.sourceforge.pmd.testframework.RuleTst;
|
||||
|
||||
@Test
|
||||
public void testVariables() throws Throwable {
|
||||
Rule rule = new XPathRule();
|
||||
rule.setProperty(XPathRule.XPATH_DESCRIPTOR, "//VariableDeclaratorId[@Image=$var]");
|
||||
rule.setMessage("Avoid vars");
|
||||
StringProperty varDescriptor = new StringProperty("var", "Test var", null, 1.0f);
|
||||
|
||||
@@ -31,9 +31,9 @@ public class XPathJspRuleTest extends RuleTst {
|
||||
Rule rule = new XPathRule();
|
||||
rule.setProperty(XPathRule.XPATH_DESCRIPTOR, XPATH_EXPRESSION);
|
||||
rule.setMessage("Test");
|
||||
rule.setLanguage(Language.JSP);
|
||||
RuleSet rules = new RuleSet();
|
||||
rules.addRule(rule);
|
||||
rules.setLanguage(Language.JSP);
|
||||
|
||||
RuleContext ctx = new RuleContext();
|
||||
Report report = new Report();
|
||||
|
||||
@@ -128,7 +128,6 @@ public abstract class RuleTst {
|
||||
ctx.setLanguageVersion(languageVersion);
|
||||
RuleSet rules = new RuleSet();
|
||||
rules.addRule(rule);
|
||||
rules.setLanguage(languageVersion.getLanguage());
|
||||
p.processFile(new StringReader(code), new RuleSets(rules), ctx);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,8 @@ public class RuleChain {
|
||||
* The RuleSet to add Rules from.
|
||||
*/
|
||||
public void add(RuleSet ruleSet) {
|
||||
Language language = ruleSet.getLanguage();
|
||||
for (Rule r : ruleSet.getRules()) {
|
||||
add(ruleSet, r, language);
|
||||
add(ruleSet, r);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +41,8 @@ public class RuleChain {
|
||||
* @param language
|
||||
* The Language used by the Rule.
|
||||
*/
|
||||
private void add(RuleSet ruleSet, Rule rule, Language language) {
|
||||
RuleChainVisitor visitor = getRuleChainVisitor(language);
|
||||
private void add(RuleSet ruleSet, Rule rule) {
|
||||
RuleChainVisitor visitor = getRuleChainVisitor(rule.getLanguage());
|
||||
if (visitor != null) {
|
||||
visitor.add(ruleSet, rule);
|
||||
}
|
||||
@@ -69,9 +68,6 @@ public class RuleChain {
|
||||
|
||||
// Get the RuleChainVisitor for the appropriate Language.
|
||||
private RuleChainVisitor getRuleChainVisitor(Language language) {
|
||||
if (language == null) {
|
||||
language = Language.JAVA;
|
||||
}
|
||||
RuleChainVisitor visitor = languageToRuleChainVisitor.get(language);
|
||||
if (visitor == null) {
|
||||
if (language.getRuleChainVisitorClass() != null) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import net.sourceforge.pmd.lang.Language;
|
||||
import net.sourceforge.pmd.lang.LanguageVersion;
|
||||
import net.sourceforge.pmd.lang.ast.Node;
|
||||
import net.sourceforge.pmd.lang.rule.RuleReference;
|
||||
import net.sourceforge.pmd.util.Benchmark;
|
||||
@@ -28,7 +29,6 @@ public class RuleSet {
|
||||
private String fileName;
|
||||
private String name = "";
|
||||
private String description = "";
|
||||
private Language language;
|
||||
private List<String> excludePatterns = new ArrayList<String>(0);
|
||||
private List<String> includePatterns = new ArrayList<String>(0);
|
||||
private Filter<File> filter;
|
||||
@@ -88,12 +88,17 @@ public class RuleSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if any rule in the RuleSet needs the DFA layer
|
||||
* Does any Rule for the given Language use the DFA layer?
|
||||
* @param language The Language.
|
||||
* @return <code>true</code> if a Rule for the Language uses the DFA layer,
|
||||
* <code>false</code> otherwise.
|
||||
*/
|
||||
public boolean usesDFA() {
|
||||
public boolean usesDFA(Language language) {
|
||||
for (Rule r : rules) {
|
||||
if (r.usesDFA()) {
|
||||
return true;
|
||||
if (r.getLanguage().equals(language)) {
|
||||
if (r.usesDFA()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -177,7 +182,7 @@ public class RuleSet {
|
||||
public void apply(List<? extends Node> acuList, RuleContext ctx) {
|
||||
long start = System.nanoTime();
|
||||
for (Rule rule : rules) {
|
||||
if (!rule.usesRuleChain()) {
|
||||
if (!rule.usesRuleChain() && applies(rule, ctx.getLanguageVersion())) {
|
||||
rule.apply(acuList, ctx);
|
||||
long end = System.nanoTime();
|
||||
Benchmark.mark(Benchmark.TYPE_RULE, rule.getName(), end - start, 1);
|
||||
@@ -186,6 +191,22 @@ public class RuleSet {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the given Rule apply to the given LanguageVersion? If so, the
|
||||
* Language must be the same and be between the minimum and maximums
|
||||
* versions on the Rule.
|
||||
*
|
||||
* @param rule The rule.
|
||||
* @param languageVersion The language version.
|
||||
*/
|
||||
public boolean applies(Rule rule, LanguageVersion languageVersion) {
|
||||
final LanguageVersion min = rule.getMinimumLanguageVersion();
|
||||
final LanguageVersion max = rule.getMinimumLanguageVersion();
|
||||
return rule.getLanguage().equals(languageVersion.getLanguage())
|
||||
&& (min == null || min.compareTo(languageVersion) <= 0)
|
||||
&& (max == null || max.compareTo(languageVersion) >= 0);
|
||||
}
|
||||
|
||||
public void end(RuleContext ctx) {
|
||||
for (Rule rule : rules) {
|
||||
rule.end(ctx);
|
||||
@@ -217,14 +238,6 @@ public class RuleSet {
|
||||
return this.getName().hashCode() + 13 * this.getRules().hashCode();
|
||||
}
|
||||
|
||||
public Language getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public void setLanguage(Language language) {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
@@ -281,10 +294,18 @@ public class RuleSet {
|
||||
this.includePatterns = includePatterns;
|
||||
}
|
||||
|
||||
public boolean usesTypeResolution() {
|
||||
/**
|
||||
* Does any Rule for the given Language use Type Resolution?
|
||||
* @param language The Language.
|
||||
* @return <code>true</code> if a Rule for the Language uses Type Resolution,
|
||||
* <code>false</code> otherwise.
|
||||
*/
|
||||
public boolean usesTypeResolution(Language language) {
|
||||
for (Rule r : rules) {
|
||||
if (r.usesTypeResolution()) {
|
||||
return true;
|
||||
if (r.getLanguage().equals(language)) {
|
||||
if (r.usesTypeResolution()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -212,7 +212,6 @@ public class RuleSetFactory {
|
||||
RuleSet ruleSet = new RuleSet();
|
||||
ruleSet.setFileName(fileName);
|
||||
ruleSet.setName(ruleSetElement.getAttribute("name"));
|
||||
ruleSet.setLanguage(Language.findByTerseName(ruleSetElement.getAttribute("language")));
|
||||
|
||||
NodeList nodeList = ruleSetElement.getChildNodes();
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
|
||||
@@ -88,10 +88,6 @@ public class RuleSetWriter {
|
||||
"http://pmd.sf.net/ruleset_xml_schema.xsd");
|
||||
ruleSetElement.setAttribute("name", ruleSet.getName());
|
||||
|
||||
if (ruleSet.getLanguage() != null) {
|
||||
ruleSetElement.setAttribute("language", ruleSet.getLanguage().getName());
|
||||
}
|
||||
|
||||
Element descriptionElement = createDescriptionElement(ruleSet.getDescription());
|
||||
ruleSetElement.appendChild(descriptionElement);
|
||||
|
||||
|
||||
@@ -96,21 +96,6 @@ public class RuleSets {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a source with given language should be checked by rules for a given
|
||||
* language. This is the case if both languages are equal, or if the source is in
|
||||
* java, and the language of the rules is unknown (for backward-compatibility
|
||||
* reasons).
|
||||
*
|
||||
* @param languageOfSource language of a source; can not be null
|
||||
* @param languageOfRule language of a ruleset; can be null
|
||||
* @return boolean true if the rule applies, else false
|
||||
*/
|
||||
public boolean applies(Language languageOfSource, Language languageOfRule) {
|
||||
return languageOfSource.equals(languageOfRule) || languageOfSource.equals(Language.JAVA)
|
||||
&& null == languageOfRule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all rules of the start of processing.
|
||||
*/
|
||||
@@ -133,11 +118,8 @@ public class RuleSets {
|
||||
public void apply(List<Node> acuList, RuleContext ctx, Language language) {
|
||||
ruleChain.apply(acuList, ctx, language);
|
||||
for (RuleSet ruleSet : ruleSets) {
|
||||
if (applies(language, ruleSet.getLanguage())) {
|
||||
// This is the finer RuleSet specific check
|
||||
if (ruleSet.applies(ctx.getSourceCodeFile())) {
|
||||
ruleSet.apply(acuList, ctx);
|
||||
}
|
||||
if (ruleSet.applies(ctx.getSourceCodeFile())) {
|
||||
ruleSet.apply(acuList, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +142,7 @@ public class RuleSets {
|
||||
*/
|
||||
public boolean usesDFA(Language language) {
|
||||
for (RuleSet ruleSet : ruleSets) {
|
||||
if (applies(language, ruleSet.getLanguage()) && ruleSet.usesDFA()) {
|
||||
if (ruleSet.usesDFA(language)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -184,7 +166,7 @@ public class RuleSets {
|
||||
|
||||
public boolean usesTypeResolution(Language language) {
|
||||
for (RuleSet ruleSet : ruleSets) {
|
||||
if (applies(language, ruleSet.getLanguage()) && ruleSet.usesTypeResolution()) {
|
||||
if (ruleSet.usesTypeResolution(language)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ public abstract class AbstractRuleChainVisitor implements RuleChainVisitor {
|
||||
int visits = 0;
|
||||
start = System.nanoTime();
|
||||
for (Rule rule: ruleSetRules.get(ruleSet)) {
|
||||
if (!ruleSet.applies(rule, ctx.getLanguageVersion())) {
|
||||
continue;
|
||||
}
|
||||
final List<String> nodeNames = rule.getRuleChainVisits();
|
||||
for (int j = 0; j < nodeNames.size(); j++) {
|
||||
List<Node> ns = nodeNameToNodes.get(nodeNames.get(j));
|
||||
|
||||
@@ -476,7 +476,6 @@ public class Designer implements ClipboardOwner {
|
||||
DFAGraphRule dfaGraphRule = new DFAGraphRule();
|
||||
RuleSet rs = new RuleSet();
|
||||
LanguageVersion languageVersion = getLanguageVersion();
|
||||
rs.setLanguage(languageVersion.getLanguage());
|
||||
if (languageVersion.getLanguage().equals(Language.JAVA)) {
|
||||
rs.addRule(dfaGraphRule);
|
||||
}
|
||||
@@ -520,13 +519,13 @@ public class Designer implements ClipboardOwner {
|
||||
}
|
||||
};
|
||||
xpathRule.setMessage("");
|
||||
xpathRule.setLanguage(getLanguageVersion().getLanguage());
|
||||
xpathRule.setProperty(XPathRule.XPATH_DESCRIPTOR, xpathQueryArea.getText());
|
||||
xpathRule.setProperty(XPathRule.VERSION_DESCRIPTOR, xpathVersionButtonGroup.getSelection()
|
||||
.getActionCommand());
|
||||
|
||||
RuleSet ruleSet = new RuleSet();
|
||||
ruleSet.addRule(xpathRule);
|
||||
ruleSet.setLanguage(getLanguageVersion().getLanguage());
|
||||
|
||||
RuleSets ruleSets = new RuleSets();
|
||||
ruleSets.addRuleSet(ruleSet);
|
||||
@@ -536,7 +535,7 @@ public class Designer implements ClipboardOwner {
|
||||
|
||||
List<Node> nodes = new ArrayList<Node>();
|
||||
nodes.add(c);
|
||||
ruleSets.apply(nodes, ruleContext, ruleSet.getLanguage());
|
||||
ruleSets.apply(nodes, ruleContext, xpathRule.getLanguage());
|
||||
|
||||
if (xpathResults.isEmpty()) {
|
||||
xpathResults.addElement("No matching nodes " + System.currentTimeMillis());
|
||||
|
||||
Reference in new issue
Block a user