Development Guides Code Styles - hiraishikentaro/rails-factorybot-jump GitHub Wiki
Rails FactoryBot Jump follows consistent TypeScript coding conventions to maintain code readability, maintainability, and team collaboration. The project uses ESLint for automated code quality enforcement.
Classes: PascalCase
// Good
class FactoryLinkProvider implements vscode.DocumentLinkProvider {
// ...
}
class CacheManager {
// ...
}
// Avoid
class factoryLinkProvider {
// ...
}
Methods and Variables: camelCase
// Good
private factoryCache: Map<string, FactoryDefinition>;
private async initializeFactoryFiles(): Promise<void> {
// ...
}
// Avoid
private factory_cache: Map<string, FactoryDefinition>;
private async initialize_factory_files(): Promise<void> {
// ...
}
Constants: SCREAMING_SNAKE_CASE
// Good
const DEFAULT_FACTORY_PATH = "spec/factories/**/*.rb";
const MAX_CACHE_SIZE = 1000;
// Avoid
const defaultFactoryPath = "spec/factories/**/*.rb";
Interfaces: PascalCase with descriptive names
// Good
interface FactoryDefinition {
uri: vscode.Uri;
lineNumber: number;
}
interface TraitDefinition extends FactoryDefinition {
factory: string;
}
// Avoid
interface IFactoryDefinition {
// Hungarian notation not used
}
Source: src/providers/factoryLinkProvider.ts
Async Functions: Always explicitly declare async
// Good
async initializeFactoryFiles(): Promise<void> {
await this.buildCache();
}
// Good for arrow functions
const processFile = async (uri: vscode.Uri): Promise<void> => {
await vscode.workspace.openTextDocument(uri);
};
// Avoid
initializeFactoryFiles() {
return this.buildCache(); // Implicit Promise return
}
Method Visibility: Be explicit about access modifiers
// Good
export class FactoryLinkProvider {
public async initializeFactoryFiles(): Promise<void> {
// Public interface
}
private async cacheFactoryDefinitions(): Promise<void> {
// Internal implementation
}
protected getConfiguration(): vscode.WorkspaceConfiguration {
// For potential subclassing
}
}
Explicit Return Types: Always specify return types for public methods
// Good
provideDocumentLinks(document: vscode.TextDocument): vscode.DocumentLink[] {
return this.generateLinks(document);
}
// Good for complex types
private buildFactoryCache(): Map<string, FactoryDefinition> {
const cache = new Map<string, FactoryDefinition>();
return cache;
}
// Avoid
provideDocumentLinks(document: vscode.TextDocument) {
return this.generateLinks(document); // Inferred return type
}
Parameter Types: Always specify parameter types
// Good
private createDocumentLink(
range: vscode.Range,
factoryName: string,
lineNumber: number
): vscode.DocumentLink {
// Implementation
}
// Avoid
private createDocumentLink(range, factoryName, lineNumber) {
// No type information
}
Import Order:
// 1. Node.js built-in modules
import * as path from "path";
import * as fs from "fs";
// 2. External dependencies
import * as vscode from "vscode";
// 3. Internal modules (relative imports)
import { FactoryDefinition } from "./types";
import { ConfigurationManager } from "../utils/config";
Import Style:
// Good: Specific imports
import { Uri, TextDocument, DocumentLink } from "vscode";
// Good: Namespace import for many items
import * as vscode from "vscode";
// Avoid: Mix of specific and namespace
import vscode, { Uri, TextDocument } from "vscode";
Consistent Class Organization:
export class FactoryLinkProvider implements vscode.DocumentLinkProvider {
// 1. Private fields
private factoryCache: Map<string, FactoryDefinition> = new Map();
private traitCache: Map<string, TraitDefinition> = new Map();
private isInitialized = false;
// 2. Constructor
constructor() {
this.initializeFactoryFiles();
}
// 3. Public interface methods
public async provideDocumentLinks(
document: vscode.TextDocument
): Promise<vscode.DocumentLink[]> {
// Implementation
}
// 4. Public utility methods
public async findFactoryFile(
factoryName: string
): Promise<vscode.Uri | undefined> {
// Implementation
}
// 5. Private implementation methods
private async initializeFactoryFiles(): Promise<void> {
// Implementation
}
private async cacheFactoryDefinitions(): Promise<void> {
// Implementation
}
}
Method Grouping: Group related methods together
// Cache management methods
private async initializeFactoryFiles(): Promise<void> {}
private async cacheFactoryDefinitions(): Promise<void> {}
private async cacheTraitDefinitions(): Promise<void> {}
// Link generation methods
private generateFactoryLinks(): vscode.DocumentLink[] {}
private generateTraitLinks(): vscode.DocumentLink[] {}
private createDocumentLink(): vscode.DocumentLink {}
// Utility methods
private getConfiguration(): vscode.WorkspaceConfiguration {}
private normalizeFactoryPath(): string {}
Graceful Error Handling:
// Good: Graceful degradation
async initializeFactoryFiles(): Promise<void> {
try {
const factoryFiles = await this.discoverFactoryFiles();
await this.processFactoryFiles(factoryFiles);
} catch (error) {
console.warn("Factory initialization failed, continuing with empty cache:", error);
// Continue with empty cache instead of failing completely
}
}
// Good: Specific error handling
private async processFactoryFile(uri: vscode.Uri): Promise<void> {
try {
const document = await vscode.workspace.openTextDocument(uri);
this.parseFactoryDefinitions(document);
} catch (error) {
console.warn(`Failed to process factory file ${uri.path}:`, error);
// Skip this file and continue with others
}
}
Consistent Error Logging:
// Good: Context-rich error messages
try {
await this.operation();
} catch (error) {
console.error("FactoryLinkProvider: Failed to initialize factory files", {
error: error.message,
stack: error.stack,
context: "initializeFactoryFiles",
});
}
// Good: User-friendly fallbacks
provideDocumentLinks(document: vscode.TextDocument): vscode.DocumentLink[] {
try {
return this.generateLinks(document);
} catch (error) {
console.error("Failed to generate document links:", error);
return []; // Return empty array instead of throwing
}
}
JSDoc Comments: For public APIs
/**
* Provides document links for factory calls in Ruby test files.
*
* @param document The text document to analyze
* @returns Array of clickable document links
*/
public async provideDocumentLinks(
document: vscode.TextDocument
): Promise<vscode.DocumentLink[]> {
// Implementation
}
/**
* Finds the factory file containing the specified factory definition.
*
* @param factoryName Name of the factory to locate
* @returns URI of the factory file, or undefined if not found
*/
public async findFactoryFile(factoryName: string): Promise<vscode.Uri | undefined> {
// Implementation
}
Inline Comments: For complex logic
// Cache factory definitions for O(1) lookup performance
private factoryCache: Map<string, FactoryDefinition> = new Map();
// Use regex to detect factory calls with various syntax patterns
const factoryCallPattern = /(?:create|build|create_list|build_list|build_stubbed|build_stubbed_list)\s*(?:\(\s*)?((:[a-zA-Z0-9_]+)(?:\s*,\s*(:[a-zA-Z0-9_]+))*)/g;
// Calculate line number by counting newlines before match position
const lines = text.substring(0, match.index).split("\n");
const lineNumber = lines.length - 1;
Structured TODO Format:
// TODO(priority): Description
// TODO(high): Implement support for factory inheritance
// TODO(medium): Add caching for regex compilation
// TODO(low): Consider supporting custom factory methods
// FIXME: Handle edge case where factory file is deleted while extension is running
// HACK: Temporary workaround for VSCode API limitation
Descriptive Test Names:
suite("Factory Detection", () => {
test("should detect basic factory calls with parentheses", () => {
// Test implementation
});
test("should detect factory calls without parentheses", () => {
// Test implementation
});
test("should handle factory calls with multiple traits", () => {
// Test implementation
});
test("should ignore factory calls in comments", () => {
// Test implementation
});
});
Consistent Test Structure:
test("should perform specific behavior", async () => {
// Arrange: Set up test data
const mockDocument = createMockDocument("user = create(:user)");
const provider = new FactoryLinkProvider();
// Act: Execute the behavior
const links = await provider.provideDocumentLinks(mockDocument);
// Assert: Verify expected outcome
assert.strictEqual(links.length, 1);
assert.ok(links[0].target?.toString().includes("gotoLine"));
});
Clear Mock Setup:
setup(() => {
// Create fresh mocks for each test
mockWorkspace = sinon.stub(vscode.workspace, "findFiles");
mockConfiguration = sinon.stub(vscode.workspace, "getConfiguration");
// Set up default behavior
mockWorkspace.resolves([]);
mockConfiguration.returns({
get: sinon
.stub()
.withArgs("factoryPaths")
.returns(["spec/factories/**/*.rb"]),
});
});
teardown(() => {
// Clean up all mocks
sinon.restore();
});
TypeScript ESLint Rules (.eslintrc.json
):
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/prefer-const": "error",
"prefer-arrow-callback": "error",
"no-var": "error"
}
}
Source: package.json#L82-L83
Linting Commands:
# Run linting
npm run lint
# Fix auto-fixable issues
npm run lint -- --fix
# Lint specific files
npm run lint src/providers/factoryLinkProvider.ts
- Use consistent naming conventions throughout the codebase
- Maintain consistent indentation (2 spaces)
- Follow the established import order
- Use explicit types for public APIs
- Handle errors gracefully without breaking user experience
- Use appropriate data structures (Map for O(1) lookups)
- Cache expensive operations
- Minimize regex recompilation
- Use lazy initialization where appropriate
- Keep methods focused and single-purpose
- Use descriptive variable and function names
- Add comments for complex business logic
- Write comprehensive tests for new features
Source: src/providers/factoryLinkProvider.ts
Following these code style guidelines ensures the Rails FactoryBot Jump extension maintains high code quality, readability, and consistency across all contributions.