Feature Sliced ESLint Plugin Bug Rules Not Catching Imports With Same Slice Name Across Layers

by StackCamp Team 95 views

Hey guys! Let's dive into a tricky bug we've discovered in the Feature Sliced ESLint plugin. This issue prevents the plugin from correctly identifying imports from segments of other layers when those layers share the same slice name. This can lead to some unexpected behavior, especially in larger projects adhering to the Feature Sliced Design (FSD) methodology. So, let's break it down and see how we can fix it!

The Problem: Same Slice, Different Layers

The core issue lies in how the layers-slices and public-api rules within the ESLint plugin operate. These rules are designed to enforce the architectural principles of FSD, ensuring that layers interact in a controlled manner, primarily through public APIs. However, the current implementation has a blind spot: it doesn't always catch imports that bypass these public APIs when slices have the same name across different layers.

To illustrate this, consider a scenario where you have a policies slice in both the pages layer and the entities layer. According to FSD, the pages layer should only import from the entities layer via its public API. This means accessing modules directly within segments like api or lib should be flagged as an error. Unfortunately, this isn't happening in certain cases.

Here’s a real-world example:

// File: src/pages/policies/ui/PolicyNodeDetailsPage.vue
import { getNodePolicyById } from "@/entities/policies/api";
import { createNodePolicyFields } from "@/entities/policies/lib";
import type { PolicyDetailField } from "@/entities/policies/lib";

In this snippet, the PolicyNodeDetailsPage.vue component, residing in the pages layer, is directly importing from the api and lib segments of the policies slice within the entities layer. This violates FSD principles, as it bypasses the intended public API (e.g., @/entities/policies). Ideally, the ESLint plugin should flag these imports as errors and suggest using the public API instead. But currently, it doesn't.

Why is this happening?

The root cause lies within the isSameSlice calculation in the src/lib/feature-sliced/extract-paths-info.ts file. Specifically, the logic at lines 36-37:

const isSameSlice = target.validatedFeatureSlicedParts.hasSlice && currentFile.validatedFeatureSlicedParts.hasSlice
  && target.fsdParts.slice === currentFile.fsdParts.slice;

This code snippet checks if the slice names are identical (policies === policies). While this is a necessary check, it's insufficient on its own. The critical missing piece is a check to ensure that the slices also belong to the same layer. The current logic doesn't differentiate between a policies slice in the pages layer and a policies slice in the entities layer. As long as the slice names match, isSameSlice evaluates to true, even when the layers are distinct (pages !== entities).

When isSameSlice returns true incorrectly, it triggers a cascade of issues:

  1. The layers-slices rule skips validation because of the isNotSuitableForValidation function in src/rules/layers-slices/model/is-not-suitable-for-validation.ts (lines 14-16).
  2. Similarly, the public-api rule bypasses validation due to the shouldBeFromSlicePublicApi function in src/rules/public-api/model/should-be-from-public-api.ts (lines 19-22).

In essence, the incorrect isSameSlice calculation effectively disables the intended checks for these imports, allowing architectural violations to slip through the cracks. This can lead to tightly coupled code and make it harder to maintain and evolve your application over time.

Expected Behavior: Catching the Violations

So, what should the ESLint plugin do in these situations? The expected behavior is clear: it should flag these direct imports from internal segments of other layers as violations. For the example above, the plugin should report an error similar to:

  • @conarti/feature-sliced/public-api should report: "Absolute imports are only allowed from public api"
  • Suggesting to use @/entities/policies instead

This error message clearly communicates the issue and provides a concrete suggestion for how to fix it. By enforcing the use of public APIs, the plugin helps maintain a clear separation of concerns and ensures that layers interact in a predictable and controlled manner.

The Suggested Fix: Adding a Layer Check

The solution to this bug is relatively straightforward. We need to modify the isSameSlice calculation to include a check for the layer. This will ensure that the rule only considers slices to be the “same” if they belong to the same layer and have the same name.

The proposed fix involves adding a simple condition to the existing isSameSlice logic:

const isSameSlice = target.validatedFeatureSlicedParts.hasSlice 
  && currentFile.validatedFeatureSlicedParts.hasSlice
  && target.fsdParts.layer === currentFile.fsdParts.layer  // Add this check
  && target.fsdParts.slice === currentFile.fsdParts.slice;

By adding target.fsdParts.layer === currentFile.fsdParts.layer, we ensure that the function only returns true if the slices are in the same layer. This prevents the rules from incorrectly skipping validation for imports that violate FSD principles.

Configuration Details

For those who are curious about the ESLint configuration used in the affected project, here’s a snippet:

featureSliced({
  publicApi: {
    level: "segments",
  },
}),
{
  rules: {
    "@conarti/feature-sliced/layers-slices": "warn",
  },
}

This configuration utilizes the featureSliced plugin and sets the publicApi level to segments. This means that the plugin should enforce public API boundaries at the segment level, ensuring that components within a slice primarily interact with other slices through their respective public APIs. The layers-slices rule is set to warn, meaning that violations will be flagged as warnings but won't prevent the build from succeeding.

Why This Matters: The Importance of Layer Isolation

This bug highlights the importance of enforcing layer isolation in FSD. When layers are allowed to directly import from each other's internal segments, it can lead to tight coupling and a breakdown of the architectural principles that FSD aims to uphold. Imagine a scenario where a component in the pages layer is directly accessing the internal implementation details of a service in the entities layer.

If the implementation of that service changes, it could potentially break the component in the pages layer, even if the public API of the entities layer remains the same. This kind of tight coupling makes it difficult to refactor and maintain the application, as changes in one area can have unintended consequences in other areas.

By enforcing the use of public APIs, we create a clear separation of concerns and reduce the risk of these kinds of cascading changes. Public APIs act as a contract between layers, providing a stable interface that allows layers to evolve independently. This makes it easier to maintain and scale the application over time.

Impact on Feature Sliced Design

The Feature Sliced Design (FSD) methodology is all about structuring your application in a way that promotes maintainability, scalability, and collaboration. One of the core principles of FSD is the separation of concerns through the use of layers and slices. Layers represent different levels of abstraction within your application, while slices group related features together.

The bug we've discussed directly undermines this principle by allowing components to bypass layer boundaries and directly access internal implementations. This can lead to a tangled codebase that's difficult to understand and maintain. By fixing this bug, we ensure that the ESLint plugin accurately enforces the principles of FSD, helping developers build more robust and maintainable applications.

Real-World Implications

This issue has significant implications for projects that heavily rely on FSD. In real-world applications, it's common to have slices with the same name across different layers. For example, a users slice might exist in both the entities layer (representing the domain model) and the features layer (representing user-related features).

Without the fix, developers might unknowingly import from the internal segments of the users slice in the entities layer from components in the features layer, violating the intended architectural boundaries. This can lead to subtle bugs and make it harder to reason about the codebase. By addressing this issue, we prevent these kinds of accidental violations and ensure that the project adheres to the intended architecture.

Conclusion: A Step Towards Robust FSD Enforcement

In conclusion, this bug in the Feature Sliced ESLint plugin highlights the importance of precise rule implementation when enforcing architectural patterns. The seemingly small oversight in the isSameSlice calculation had significant implications for the enforcement of FSD principles. By adding a simple check for the layer, we can prevent unintended violations of layer boundaries and ensure that the plugin accurately reflects the intended architecture.

This fix represents a step towards more robust FSD enforcement, helping developers build maintainable and scalable applications. By catching these kinds of issues early in the development process, we can prevent them from becoming larger problems down the road. Remember, guys, a well-structured codebase is key to long-term success!

We hope this deep dive into the bug and its fix has been insightful. Stay tuned for more updates and discussions on Feature Sliced Design and best practices!