Skip to content

Commit

Permalink
Merge pull request #369 from bmish/rule-routes-path-style
Browse files Browse the repository at this point in the history
Add new 'route-path-style' rule
  • Loading branch information
Turbo87 committed Mar 18, 2019
2 parents 145d8df + 8d7c8b1 commit ce20118
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ The `--fix` option on the command line automatically fixes problems reported by
| | [no-unnecessary-index-route](./docs/rules/no-unnecessary-index-route.md) | Disallow unnecessary `index` route definition |
| :wrench: | [no-unnecessary-route-path-option](./docs/rules/no-unnecessary-route-path-option.md) | Disallow unnecessary route `path` option |
| :wrench: | [no-unnecessary-service-injection-argument](./docs/rules/no-unnecessary-service-injection-argument.md) | Disallow unnecessary argument when injecting service |
| | [route-path-style](./docs/rules/route-path-style.md) | Enforces usage of kebab-case (instead of snake_case or camelCase) in route paths |
| :wrench: | [use-ember-get-and-set](./docs/rules/use-ember-get-and-set.md) | Enforces usage of Ember.get and Ember.set |


Expand Down
10 changes: 10 additions & 0 deletions docs/rules/no-capital-letters-in-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ this.route('home');
this.route('sign-up');

```

### References

* [Ember Routing Guide](https://guides.emberjs.com/release/routing/)

### Related Rules

* [no-unnecessary-route-path-option](no-unnecessary-route-path-option.md)
* [route-path-style](route-path-style.md)
* [routes-segments-snake-case](routes-segments-snake-case.md)
1 change: 1 addition & 0 deletions docs/rules/no-unnecessary-route-path-option.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ this.route('blog-posts', { path: '/blog' });

* [no-capital-letters-in-routes](no-capital-letters-in-routes.md)
* [no-unnecessary-index-route](no-unnecessary-index-route.md)
* [route-path-style](route-path-style.md)
* [routes-segments-snake-case](routes-segments-snake-case.md)
48 changes: 48 additions & 0 deletions docs/rules/route-path-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## Enforces usage of kebab-case (instead of snake_case or camelCase) in route paths

### Rule name: `route-path-style`

A best practice on the web is to use kebab-case (hyphens) for separating words in URLs. This style is good for readability, clarity, SEO, etc.

Example kebab-case URL: `https://guides.emberjs.com/release/getting-started/core-concepts/`

### Rule Details

Examples of **incorrect** code for this rule:

```js
this.route('blog_posts');
```

```js
this.route('blogPosts');
```

```js
this.route('blog-posts', { path: '/blog_posts' });
```

```js
this.route('blog-posts', { path: '/blogPosts' });
```

Examples of **correct** code for this rule:

```js
this.route('blog-posts');
```

```js
this.route('blog_posts', { path: '/blog-posts' });
```

### References

* [Ember Routing Guide](https://guides.emberjs.com/release/routing/)
* [Keep a simple URL structure](https://support.google.com/webmasters/answer/76329) article by Google

### Related Rules

* [no-capital-letters-in-routes](no-capital-letters-in-routes.md)
* [no-unnecessary-route-path-option](no-unnecessary-route-path-option.md)
* [routes-segments-snake-case](routes-segments-snake-case.md)
10 changes: 10 additions & 0 deletions docs/rules/routes-segments-snake-case.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ this.route('tree', { path: ':tree_id'});
// BAD
this.route('tree', { path: ':treeId'});
```

### References

* [Ember Routing Guide](https://guides.emberjs.com/release/routing/)

### Related Rules

* [no-capital-letters-in-routes](no-capital-letters-in-routes.md)
* [no-unnecessary-route-path-option](no-unnecessary-route-path-option.md)
* [route-path-style](route-path-style.md)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module.exports = {
'order-in-controllers': require('./rules/order-in-controllers'),
'order-in-models': require('./rules/order-in-models'),
'order-in-routes': require('./rules/order-in-routes'),
'route-path-style': require('./rules/route-path-style'),
'require-return-from-computed': require('./rules/require-return-from-computed'),
'require-super-in-init': require('./rules/require-super-in-init'),
'routes-segments-snake-case': require('./rules/routes-segments-snake-case'),
Expand Down
3 changes: 2 additions & 1 deletion lib/recommended-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ module.exports = {
"ember/order-in-routes": "off",
"ember/require-return-from-computed": "off",
"ember/require-super-in-init": "error",
"ember/route-path-style": "off",
"ember/routes-segments-snake-case": "error",
"ember/use-brace-expansion": "error",
"ember/use-ember-get-and-set": "off"
}
}
73 changes: 73 additions & 0 deletions lib/rules/route-path-style.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';

const utils = require('../utils/utils');
const ember = require('../utils/ember');

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

const ERROR_MESSAGE = 'Use kebab-case in route path.';

module.exports = {
meta: {
docs: {
description: 'Enforces usage of kebab-case (instead of snake_case or camelCase) in route paths',
category: 'Best Practices',
recommended: false
},
fixable: null,
ERROR_MESSAGE
},

create(context) {
return {
CallExpression(node) {
if (!ember.isRoute(node)) {
return;
}

// Retrieve the path based on the call format used:
// 1. this.route('route-and-path');
// 2. this.route('route', { path: 'path' });

const hasExplicitPathOption = utils.isObjectExpression(node.arguments[1]) && hasPropertyWithKeyName(node.arguments[1], 'path');
const pathValueNode = hasExplicitPathOption ? getPropertyByKeyName(node.arguments[1], 'path').value : node.arguments[0];
const pathValue = pathValueNode.value;

const urlSegments = getStaticURLSegments(pathValue);

if (urlSegments.some(urlPart => !isKebabCase(urlPart))) {
context.report(pathValueNode, ERROR_MESSAGE);
}
},
};
}
};

function hasPropertyWithKeyName(objectExpression, keyName) {
return getPropertyByKeyName(objectExpression, keyName) !== undefined;
}

function getPropertyByKeyName(objectExpression, keyName) {
return objectExpression.properties.find(property => property.key.name === keyName);
}

function getStaticURLSegments(path) {
return path
.split('/')
.filter(segment => !isDynamicSegment(segment) && !isWildcardSegment(segment) && segment !== '');
}

function isDynamicSegment(segment) {
return segment.includes(':');
}

function isWildcardSegment(segment) {
return segment.includes('*');
}

const KEBAB_CASE_REGEXP = /^[0-9a-z-]+$/;
function isKebabCase(str) {
return str.match(KEBAB_CASE_REGEXP);
}
117 changes: 117 additions & 0 deletions tests/lib/rules/route-path-style.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require('../../../lib/rules/route-path-style');
const RuleTester = require('eslint').RuleTester;

const { ERROR_MESSAGE } = rule;

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester();
ruleTester.run('route-path-style', rule, {
valid: [
// Implicit path:
'this.route("blog");',
'this.route("blog-posts");',

// Explicit path:
'this.route("blog", { path: "" });',
'this.route("blog", { path: "/" });',
'this.route("blog", { path: "*:" });',
'this.route("blog", { path: "/blog" });',
'this.route("blog", { path: "/blog/blog" });',
'this.route("blog", { path: "/blog/blog-posts" });',
'this.route("blog-posts", { path: "/blog-posts" });',
'this.route("blog_posts", { path: "/blog-posts" });',
'this.route("blogPosts", { path: "/blog-posts" });',

// With dynamic segments:
'this.route("blog", { path: "/blog/blog-posts/:blog_id" });',
'this.route("blog", { path: ":blog_id" });',
'this.route("blog", { path: "blog/:blog_id" });',
'this.route("blog", { path: "/blog/:blog_id" });',
'this.route("blog", { path: ":blog_id/:other_id" });',
'this.route("blog", { path: "/blog/:blog_id/test/:other_id" });',

// With wildcard segment:
'this.route("blog", { path: "*path" });',
'this.route("blog", { path: "*path_name" });',
'this.route("blog", { path: "*pathName" });',
'this.route("blog", { path: "/*path" });',
'this.route("blog", { path: "/*path_name" });',
'this.route("blog", { path: "/*pathName" });',
'this.route("blog", { path: "/blog/*path" });',
'this.route("blog", { path: "/blog/*path_name" });',
'this.route("blog", { path: "/blog/*pathName" });',

// With function:
'this.route("blog", function() { this.route("update"); });',
'this.route("blog", { path: "/blog-posts" }, function() { this.route("update"); });',

// With other field in object:
'this.route("blog", { otherField: "/blog_posts" });',
'this.route("blog", { otherField: "/blog_posts", path: "/blog" });',
'this.route("blog-posts", { otherField: "/blog_posts" });',

// Not Ember's route function:
'test();',
"test('blog');",
"test('blog_posts');",
"test('blogPosts');",
"test('blog-posts', { path: '/blog_posts' });",
'this.test();',
"this.test('blog');",
"this.test('blog_posts');",
"this.test('blogPosts');",
"this.test('blog-posts', { path: '/blog_posts' });",
'MyClass.route();',
"MyClass.route('blog');",
"MyClass.route('blog_posts');",
"MyClass.route('blogPosts');",
"MyClass.route('blog-posts', { path: '/blog_posts' });",
"route.unrelatedFunction('blog_posts');",
"this.route.unrelatedFunction('blog_posts');"
],
invalid: [
{
code: 'this.route("blog_posts");',
errors: [{ message: ERROR_MESSAGE, type: 'Literal' }]
},
{
code: 'this.route("blogPosts");',
errors: [{ message: ERROR_MESSAGE, type: 'Literal' }]
},
{
code: 'this.route("blogPosts", function() {});',
errors: [{ message: ERROR_MESSAGE, type: 'Literal' }]
},
{
code: 'this.route("blog-posts", { path: "/blog_posts" });',
errors: [{ message: ERROR_MESSAGE, type: 'Literal' }]
},
{
code: 'this.route("blog-posts", { path: "/blogPosts" });',
errors: [{ message: ERROR_MESSAGE, type: 'Literal' }]
},
{
code: 'this.route("blog-posts", { path: "/blogPosts/:blog_id" });',
errors: [{ message: ERROR_MESSAGE, type: 'Literal' }]
},
{
code: 'this.route("blog-posts", { path: "/blog-posts/:blog_id/otherPart" });',
errors: [{ message: ERROR_MESSAGE, type: 'Literal' }]
},
{
code: 'this.route("blog-posts", { path: "/blogPosts/*path" });',
errors: [{ message: ERROR_MESSAGE, type: 'Literal' }]
},
{
code: 'this.route("blog-posts", { path: "/blogPosts" }, function() {});',
errors: [{ message: ERROR_MESSAGE, type: 'Literal' }]
}
]
});

0 comments on commit ce20118

Please sign in to comment.