or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

ast-parsing.mdast-traversal.mdcopy-paste-detection.mdindex.mdlanguage-module.mdrule-development.md

rule-development.mddocs/

0

# Rule Development

1

2

Rule development framework provides the foundation for creating custom static analysis rules for Scala code. The framework integrates with PMD's rule engine and provides Scala-specific base classes and visitor patterns for comprehensive code analysis.

3

4

## Core Rule Development Components

5

6

### ScalaRule Base Class

7

8

Primary base class for developing Scala-specific PMD rules with integrated visitor pattern support.

9

10

```java { .api }

11

public class ScalaRule extends AbstractRule implements ScalaVisitor<RuleContext, RuleContext> {

12

public void apply(Node target, RuleContext ctx);

13

public RuleContext visitNode(Node node, RuleContext param);

14

15

// Default visitor implementations for all node types

16

public RuleContext visit(ScalaNode<?> node, RuleContext data);

17

public RuleContext visit(ASTSource node, RuleContext data);

18

19

// Override specific visit methods for targeted analysis

20

public RuleContext visit(ASTDefnClass node, RuleContext data);

21

public RuleContext visit(ASTDefnDef node, RuleContext data);

22

public RuleContext visit(ASTTermApply node, RuleContext data);

23

// ... methods for all 140+ AST node types

24

}

25

```

26

27

**Basic Rule Implementation**:

28

29

```java

30

public class ClassComplexityRule extends ScalaRule {

31

private static final int MAX_METHODS = 15;

32

33

@Override

34

public RuleContext visit(ASTDefnClass node, RuleContext ctx) {

35

// Count methods in this class

36

long methodCount = node.descendants(ASTDefnDef.class).count();

37

38

if (methodCount > MAX_METHODS) {

39

ctx.addViolation(node,

40

"Class '" + getClassName(node) + "' has too many methods: " +

41

methodCount + " (max: " + MAX_METHODS + ")");

42

}

43

44

// Continue visiting child nodes

45

return super.visit(node, ctx);

46

}

47

48

private String getClassName(ASTDefnClass classNode) {

49

// Extract class name from AST node

50

return classNode.descendants(ASTTermName.class)

51

.findFirst()

52

.map(name -> name.getText())

53

.orElse("Unknown");

54

}

55

}

56

```

57

58

## Rule Implementation Patterns

59

60

### Method-Level Analysis Rules

61

62

```java

63

public class MethodNamingRule extends ScalaRule {

64

private static final Pattern CAMEL_CASE = Pattern.compile("^[a-z][a-zA-Z0-9]*$");

65

66

@Override

67

public RuleContext visit(ASTDefnDef node, RuleContext ctx) {

68

String methodName = extractMethodName(node);

69

70

if (!CAMEL_CASE.matcher(methodName).matches()) {

71

ctx.addViolation(node,

72

"Method name '" + methodName + "' should be in camelCase");

73

}

74

75

// Check method length

76

int lineCount = node.getEndLine() - node.getBeginLine() + 1;

77

if (lineCount > 50) {

78

ctx.addViolation(node,

79

"Method '" + methodName + "' is too long: " + lineCount + " lines");

80

}

81

82

return super.visit(node, ctx);

83

}

84

85

private String extractMethodName(ASTDefnDef methodNode) {

86

return methodNode.descendants(ASTTermName.class)

87

.findFirst()

88

.map(name -> name.getText())

89

.orElse("unknown");

90

}

91

}

92

```

93

94

### Expression-Level Analysis Rules

95

96

```java

97

public class NestedBlockRule extends ScalaRule {

98

private static final int MAX_NESTING_DEPTH = 4;

99

100

@Override

101

public RuleContext visit(ASTTermBlock node, RuleContext ctx) {

102

int nestingDepth = calculateNestingDepth(node);

103

104

if (nestingDepth > MAX_NESTING_DEPTH) {

105

ctx.addViolation(node,

106

"Code block is nested too deeply: " + nestingDepth +

107

" levels (max: " + MAX_NESTING_DEPTH + ")");

108

}

109

110

return super.visit(node, ctx);

111

}

112

113

private int calculateNestingDepth(ASTTermBlock block) {

114

int depth = 0;

115

ScalaNode<?> current = block.getParent();

116

117

while (current != null) {

118

if (current instanceof ASTTermBlock ||

119

current instanceof ASTTermIf ||

120

current instanceof ASTTermMatch ||

121

current instanceof ASTTermFor) {

122

depth++;

123

}

124

current = current.getParent();

125

}

126

127

return depth;

128

}

129

}

130

```

131

132

### Pattern Matching Analysis Rules

133

134

```java

135

public class MatchExpressionRule extends ScalaRule {

136

private static final int MAX_CASES = 10;

137

138

@Override

139

public RuleContext visit(ASTTermMatch node, RuleContext ctx) {

140

// Count case clauses

141

long caseCount = node.descendants(ASTCase.class).count();

142

143

if (caseCount > MAX_CASES) {

144

ctx.addViolation(node,

145

"Match expression has too many cases: " + caseCount +

146

" (max: " + MAX_CASES + ")");

147

}

148

149

// Check for wildcard patterns

150

boolean hasWildcard = node.descendants(ASTPatWildcard.class).count() > 0;

151

if (!hasWildcard) {

152

ctx.addViolation(node,

153

"Match expression should include a wildcard case for safety");

154

}

155

156

return super.visit(node, ctx);

157

}

158

159

@Override

160

public RuleContext visit(ASTCase node, RuleContext ctx) {

161

// Analyze individual case patterns

162

analyzeCasePattern(node, ctx);

163

return super.visit(node, ctx);

164

}

165

166

private void analyzeCasePattern(ASTCase caseNode, RuleContext ctx) {

167

// Check for complex nested patterns

168

long patternDepth = caseNode.descendants(ASTPatExtract.class)

169

.mapToInt(this::getPatternDepth)

170

.max()

171

.orElse(0);

172

173

if (patternDepth > 3) {

174

ctx.addViolation(caseNode,

175

"Case pattern is too complex (depth: " + patternDepth + ")");

176

}

177

}

178

179

private int getPatternDepth(ASTPatExtract pattern) {

180

// Calculate nesting depth of pattern extractors

181

int depth = 1;

182

pattern.descendants(ASTPatExtract.class).forEach(nested -> depth++);

183

return depth;

184

}

185

}

186

```

187

188

## Import and Package Rules

189

190

### Import Organization Rules

191

192

```java

193

public class ImportOrganizationRule extends ScalaRule {

194

@Override

195

public RuleContext visit(ASTImport node, RuleContext ctx) {

196

// Check for wildcard imports

197

if (hasWildcardImport(node)) {

198

String importPath = extractImportPath(node);

199

if (!isAllowedWildcardImport(importPath)) {

200

ctx.addViolation(node,

201

"Avoid wildcard imports except for allowed packages: " + importPath);

202

}

203

}

204

205

// Check for unused imports (requires additional analysis)

206

checkUnusedImports(node, ctx);

207

208

return super.visit(node, ctx);

209

}

210

211

private boolean hasWildcardImport(ASTImport importNode) {

212

return importNode.descendants(ASTImporteeWildcard.class).count() > 0;

213

}

214

215

private boolean isAllowedWildcardImport(String importPath) {

216

return importPath.startsWith("scala.collection.") ||

217

importPath.startsWith("java.util.") ||

218

importPath.equals("scala.concurrent.ExecutionContext.Implicits");

219

}

220

221

private String extractImportPath(ASTImport importNode) {

222

return importNode.descendants(ASTImporter.class)

223

.findFirst()

224

.map(imp -> imp.getText())

225

.orElse("");

226

}

227

228

private void checkUnusedImports(ASTImport importNode, RuleContext ctx) {

229

// Complex analysis requiring symbol resolution

230

// Implementation would check if imported symbols are used in the file

231

}

232

}

233

```

234

235

### Package Naming Rules

236

237

```java

238

public class PackageNamingRule extends ScalaRule {

239

private static final Pattern PACKAGE_PATTERN =

240

Pattern.compile("^[a-z]+(\\.[a-z][a-z0-9]*)*$");

241

242

@Override

243

public RuleContext visit(ASTPkg node, RuleContext ctx) {

244

String packageName = extractPackageName(node);

245

246

if (!PACKAGE_PATTERN.matcher(packageName).matches()) {

247

ctx.addViolation(node,

248

"Package name '" + packageName +

249

"' should be lowercase with dots as separators");

250

}

251

252

return super.visit(node, ctx);

253

}

254

255

private String extractPackageName(ASTPkg packageNode) {

256

return packageNode.getText().replaceFirst("package\\s+", "").trim();

257

}

258

}

259

```

260

261

## Type and Generic Rules

262

263

### Generic Type Usage Rules

264

265

```java

266

public class GenericTypeRule extends ScalaRule {

267

@Override

268

public RuleContext visit(ASTTypeParam node, RuleContext ctx) {

269

String paramName = node.getText();

270

271

// Check type parameter naming convention

272

if (!paramName.matches("^[A-Z][A-Za-z0-9]*$")) {

273

ctx.addViolation(node,

274

"Type parameter '" + paramName +

275

"' should start with uppercase letter");

276

}

277

278

// Check for single-letter type parameters (often preferred)

279

if (paramName.length() > 2 && isSingleConceptType(paramName)) {

280

ctx.addViolation(node,

281

"Consider using single-letter type parameter: " + paramName);

282

}

283

284

return super.visit(node, ctx);

285

}

286

287

@Override

288

public RuleContext visit(ASTTypeApply node, RuleContext ctx) {

289

// Check for raw types (missing type parameters)

290

checkRawTypeUsage(node, ctx);

291

return super.visit(node, ctx);

292

}

293

294

private boolean isSingleConceptType(String paramName) {

295

return paramName.equals("Element") ||

296

paramName.equals("Value") ||

297

paramName.equals("Key");

298

}

299

300

private void checkRawTypeUsage(ASTTypeApply typeApp, RuleContext ctx) {

301

// Analysis of generic type applications

302

String typeName = extractTypeName(typeApp);

303

if (isGenericType(typeName) && !hasTypeArguments(typeApp)) {

304

ctx.addViolation(typeApp,

305

"Raw type usage detected: " + typeName +

306

" should specify type parameters");

307

}

308

}

309

310

private boolean isGenericType(String typeName) {

311

return typeName.equals("List") ||

312

typeName.equals("Map") ||

313

typeName.equals("Option") ||

314

typeName.equals("Future");

315

}

316

317

private boolean hasTypeArguments(ASTTypeApply typeApp) {

318

return typeApp.getNumChildren() > 1;

319

}

320

321

private String extractTypeName(ASTTypeApply typeApp) {

322

return typeApp.descendants(ASTTypeName.class)

323

.findFirst()

324

.map(name -> name.getText())

325

.orElse("");

326

}

327

}

328

```

329

330

## Rule Configuration and Properties

331

332

### Configurable Rule Properties

333

334

```java

335

public class ConfigurableComplexityRule extends ScalaRule {

336

private static final IntegerProperty MAX_METHODS =

337

IntegerProperty.named("maxMethods")

338

.desc("Maximum number of methods per class")

339

.defaultValue(15)

340

.range(5, 50)

341

.build();

342

343

private static final BooleanProperty IGNORE_PRIVATE =

344

BooleanProperty.named("ignorePrivateMethods")

345

.desc("Ignore private methods in count")

346

.defaultValue(false)

347

.build();

348

349

public ConfigurableComplexityRule() {

350

definePropertyDescriptor(MAX_METHODS);

351

definePropertyDescriptor(IGNORE_PRIVATE);

352

}

353

354

@Override

355

public RuleContext visit(ASTDefnClass node, RuleContext ctx) {

356

int maxMethods = getProperty(MAX_METHODS);

357

boolean ignorePrivate = getProperty(IGNORE_PRIVATE);

358

359

long methodCount = countMethods(node, ignorePrivate);

360

361

if (methodCount > maxMethods) {

362

ctx.addViolation(node,

363

"Class has " + methodCount + " methods (max: " + maxMethods + ")");

364

}

365

366

return super.visit(node, ctx);

367

}

368

369

private long countMethods(ASTDefnClass classNode, boolean ignorePrivate) {

370

Stream<ASTDefnDef> methods = classNode.descendants(ASTDefnDef.class);

371

372

if (ignorePrivate) {

373

methods = methods.filter(method -> !isPrivateMethod(method));

374

}

375

376

return methods.count();

377

}

378

379

private boolean isPrivateMethod(ASTDefnDef method) {

380

return method.descendants(ASTModPrivate.class).count() > 0;

381

}

382

}

383

```

384

385

## Advanced Rule Techniques

386

387

### Multi-Pass Analysis

388

389

```java

390

public class CrossReferenceRule extends ScalaRule {

391

private final Map<String, List<ASTDefnClass>> classRegistry = new HashMap<>();

392

private final Set<String> usedClasses = new HashSet<>();

393

394

@Override

395

public void start(RuleContext ctx) {

396

// First pass: collect all class definitions

397

super.start(ctx);

398

classRegistry.clear();

399

usedClasses.clear();

400

}

401

402

@Override

403

public RuleContext visit(ASTDefnClass node, RuleContext ctx) {

404

String className = extractClassName(node);

405

classRegistry.computeIfAbsent(className, k -> new ArrayList<>()).add(node);

406

return super.visit(node, ctx);

407

}

408

409

@Override

410

public RuleContext visit(ASTTermNew node, RuleContext ctx) {

411

// Track class usage in object instantiation

412

String usedClass = extractInstantiatedClass(node);

413

usedClasses.add(usedClass);

414

return super.visit(node, ctx);

415

}

416

417

@Override

418

public void end(RuleContext ctx) {

419

// Second pass: check for unused classes

420

classRegistry.forEach((className, instances) -> {

421

if (!usedClasses.contains(className) && !isExcluded(className)) {

422

instances.forEach(classNode ->

423

ctx.addViolation(classNode,

424

"Class '" + className + "' appears to be unused"));

425

}

426

});

427

428

super.end(ctx);

429

}

430

431

private boolean isExcluded(String className) {

432

return className.endsWith("Test") ||

433

className.endsWith("Spec") ||

434

className.equals("Main");

435

}

436

}

437

```

438

439

### Context-Aware Rules

440

441

```java

442

public class ContextAwareRule extends ScalaRule {

443

private final Deque<String> contextStack = new ArrayDeque<>();

444

445

@Override

446

public RuleContext visit(ASTDefnClass node, RuleContext ctx) {

447

String className = extractClassName(node);

448

contextStack.push("class:" + className);

449

450

RuleContext result = super.visit(node, ctx);

451

452

contextStack.pop();

453

return result;

454

}

455

456

@Override

457

public RuleContext visit(ASTDefnDef node, RuleContext ctx) {

458

String methodName = extractMethodName(node);

459

String currentContext = String.join("/", contextStack);

460

461

// Apply context-specific rules

462

if (currentContext.contains("Test")) {

463

checkTestMethodNaming(node, methodName, ctx);

464

} else {

465

checkRegularMethodNaming(node, methodName, ctx);

466

}

467

468

return super.visit(node, ctx);

469

}

470

471

private void checkTestMethodNaming(ASTDefnDef node, String methodName, RuleContext ctx) {

472

if (!methodName.startsWith("test") && !methodName.contains("should")) {

473

ctx.addViolation(node,

474

"Test method '" + methodName +

475

"' should start with 'test' or contain 'should'");

476

}

477

}

478

479

private void checkRegularMethodNaming(ASTDefnDef node, String methodName, RuleContext ctx) {

480

if (!methodName.matches("^[a-z][a-zA-Z0-9]*$")) {

481

ctx.addViolation(node,

482

"Method '" + methodName + "' should be camelCase");

483

}

484

}

485

}

486

```

487

488

## Integration with PMD Framework

489

490

### Rule Registration

491

492

Rules are registered with PMD through rule set XML files:

493

494

```xml

495

<?xml version="1.0"?>

496

<ruleset name="Custom Scala Rules">

497

<description>Custom static analysis rules for Scala code</description>

498

499

<rule name="ClassComplexity"

500

language="scala"

501

class="com.example.rules.ClassComplexityRule">

502

<description>Checks for classes with too many methods</description>

503

<properties>

504

<property name="maxMethods" value="15"/>

505

<property name="ignorePrivateMethods" value="false"/>

506

</properties>

507

</rule>

508

509

<rule name="MethodNaming"

510

language="scala"

511

class="com.example.rules.MethodNamingRule">

512

<description>Enforces camelCase method naming</description>

513

</rule>

514

</ruleset>

515

```

516

517

### Rule Testing

518

519

```java

520

// Unit test for rule validation

521

public class ClassComplexityRuleTest {

522

@Test

523

public void testClassWithTooManyMethods() {

524

String code = """

525

class LargeClass {

526

def method1() = {}

527

def method2() = {}

528

// ... more methods

529

def method20() = {}

530

}

531

""";

532

533

Rule rule = new ClassComplexityRule();

534

List<RuleViolation> violations = PMDTestUtils.check(rule, code);

535

536

assertEquals(1, violations.size());

537

assertTrue(violations.get(0).getDescription().contains("too many methods"));

538

}

539

}

540

```

541

542

This rule development framework provides comprehensive capabilities for creating sophisticated Scala static analysis rules that integrate seamlessly with PMD's analysis pipeline.