Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
chdsbd committed Apr 18, 2021
0 parents commit e95d9a3
Show file tree
Hide file tree
Showing 20 changed files with 4,748 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
coverage
dist
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
coverage
dist
11 changes: 11 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// https://prettier.io/docs/en/options.html
module.exports = {
semi: false,
useTabs: false,
tabWidth: 2,
singleQuote: true,
trailingComma: 'es5',
bracketSpacing: true,
jsxBracketSameLine: true,
arrowParens: 'always',
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## 0.0.1 - 2021-04-17

### Added

- added `better-dates/ban-date-mutation` rule to prevent modifying Dates
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Christopher Dignam

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# eslint-plugin-better-dates [![npm version](https://img.shields.io/npm/v/@typescript-eslint/eslint-plugin.svg)](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin)

> An ESlint plugin to prevent `Date`-related bugs.
## Installation

Make sure you have TypeScript and [`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/README.md) installed:

```console
$ yarn add -D typescript @typescript-eslint/parser
$ npm i --save-dev typescript @typescript-eslint/parser
```

Then install the plugin:

```console
$ yarn add -D eslint-plugin-better-dates
$ npm i --save-dev eslint-plugin-better-dates
```

## Usage

Add `@typescript-eslint/parser` to the `parser` field and `better-dates` to the plugins section of your `.eslintrc` configuration file, then configure the rules you want to use under the rules section.

```json
{
"parser": "@typescript-eslint/parser",
"plugins": ["better-dates"],
"rules": {
"better-dates/ban-mutation": "error"
}
}
```

**Note: Make sure to use `eslint --ext .js,.ts` since by [default](https://eslint.org/docs/user-guide/command-line-interface#--ext) `eslint` will only search for `.js` files.**

## Rules

**Key**: :wrench: = fixable, :thought_balloon: = requires type information

| Name | Description | :wrench: | :thought_balloon: |
| -------------------------------------------------------- | --------------------------------------------------------- | -------- | ----------------- |
| [`ban-date-mutation`](./docs/rules/ban-date-mutation.md) | Bans calling methods on `Date` that will mutate the date. | | :thought_balloon: |

> Project layout and configuration modified from [`typescript-eslint/eslint-plugin`](https://github.com/typescript-eslint/typescript-eslint/tree/26d71b57fbff013b9c9434c96e2ba98c6c541259/packages/eslint-plugin).
63 changes: 63 additions & 0 deletions docs/rules/ban-date-mutation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Bans mutating dates (`ban-date-mutation`)

Mutating dates can create confusing bugs. Instead, create new dates using a library like [`date-fns`](https://github.com/date-fns/date-fns).

## Rule Details

This rule bans methods that mutate `Date`.

Banned `Date` methods:

- `setDate`
- `setFullYear`
- `setHours`
- `setMilliseconds`
- `setMinutes`
- `setMonth`
- `setSeconds`
- `setTime`
- `setUTCDate`
- `setUTCFullYear`
- `setUTCHours`
- `setUTCMilliseconds`
- `setUTCMinutes`
- `setUTCMonth`
- `setUTCSeconds`
- `setYear`

### Examples

Examples of **incorrect** code with the default options:

```ts
const expiration = new Date()

const noon = expiration.setHours(12)
const seventeenth = expiration.setDate(17)
const twentyTen = expiration.setFullYear(2010, 05, 23)
const thirtySeconds = expiration.setSeconds(30)
```

Examples of **correct** code with the default options:

```ts
const expiration = new Date()

import setHours from 'date-fns/setHours'
const noon = setHours(setHours, 12)

import setDate from 'date-fns/setDate'
const seventeenth = setDate(expiration, 17)

import setYear from 'date-fns/setYear'
import setMonth from 'date-fns/setMonth'
import setDate from 'date-fns/setDate'
const twentyTen = setDate(setMonth(setYear(expiration, 2010), 05), 23)

import setSeconds from 'date-fns/setSeconds'
const thirtySeconds = setSeconds(expiration, 30)
```

## When Not To Use It

If you don't mind the risk of date mutations causing bugs in your system.
20 changes: 20 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict'

// @ts-check
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
globals: {
'ts-jest': {
isolatedModules: true,
},
},
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: './tests/.+\\.test\\.ts$',
collectCoverage: false,
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
coverageReporters: ['text-summary', 'lcov'],
}
61 changes: 61 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "eslint-plugin-better-dates",
"version": "0.0.1",
"description": "ESLint plugin to prevent Date-related bugs.",
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin",
"typescript"
],
"engines": {
"node": "^10.12.0 || >=12.0.0"
},
"files": [
"dist",
"docs",
"package.json",
"README.md",
"LICENSE"
],
"repository": {
"type": "git",
"url": "https://github.com/chdsbd/eslint-plugin-better-dates.git"
},
"bugs": {
"url": "https://github.com/chdsbd/eslint-plugin-better-dates/issues"
},
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -b tsconfig.json",
"clean": "tsc -b tsconfig.json --clean",
"format": "prettier --write \"./**/*.{ts,js,json,md}\"",
"lint": "eslint . --ext .js,.ts --ignore-path ../../.eslintignore",
"test": "jest --coverage",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@typescript-eslint/experimental-utils": "^4.22.0"
},
"devDependencies": {
"@types/node": "^14.14.41",
"@types/prettier": "*",
"@typescript-eslint/parser": "^4.22.0",
"eslint": "^7.24.0",
"jest": "^26.6.3",
"prettier": "*",
"ts-jest": "^26.5.5",
"typescript": "*"
},
"peerDependencies": {
"@typescript-eslint/parser": "^4.0.0",
"eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import rules from './rules'

export = {
rules,
}
115 changes: 115 additions & 0 deletions src/rules/ban-date-mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as ts from 'typescript'
import * as util from '../util'

export type MessageIds = 'banDateMutation'

const disallowedDateMethods = new Set([
'setDate',
'setFullYear',
'setHours',
'setMilliseconds',
'setMinutes',
'setMonth',
'setSeconds',
'setTime',
'setUTCDate',
'setUTCFullYear',
'setUTCHours',
'setUTCMilliseconds',
'setUTCMinutes',
'setUTCMonth',
'setUTCSeconds',
'setYear',
])

const methodToDateFns: Record<string, string | null> = {
setDate: 'setDate',
setFullYear: '{setYear,setMonth,setDate}',
setHours: '{setHours,setMinutes,setSeconds,setMilliseconds}',
setMilliseconds: 'setMilliseconds',
setMinutes: 'setMinutes',
setMonth: 'setMonth',
setSeconds: 'setSeconds',
setTime: 'fromUnixTime',
setUTCDate: null,
setUTCFullYear: null,
setUTCHours: null,
setUTCMilliseconds: null,
setUTCMinutes: null,
setUTCMonth: null,
setUTCSeconds: null,
setYear: 'setYear',
}

function formatSuggestion(
methodName: string,
suggestion: string | null
): string {
if (suggestion === null) {
return ''
}
return ` Replace \`Date.${methodName}\` with \`date-fns/${suggestion}\``
}

function isDate(symbol: ts.Symbol, typeChecker: ts.TypeChecker): boolean {
return (
typeChecker.typeToString(
typeChecker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration)
) === 'DateConstructor'
)
}

export default util.createRule<[], MessageIds>({
name: 'ban-date-mutation',
meta: {
type: 'suggestion',
docs: {
description: 'Bans mutating Date objects',
category: 'Best Practices',
recommended: 'error',
},
messages: {
banDateMutation: "Don't mutate a Date with `{{method}}`.{{suggestion}}",
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
MemberExpression(node): void {
const parserServices = util.getParserServices(context)
const typeChecker = parserServices.program.getTypeChecker()
const objectType = typeChecker
.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(node.object)
)
.getSymbol()
if (objectType != null && isDate(objectType, typeChecker)) {
const dateMethod = typeChecker
.getTypeAtLocation(
parserServices.esTreeNodeToTSNodeMap.get(node.property)
)
.getSymbol()
?.getEscapedName()
if (dateMethod == null) {
return
}
const dateMethodString = dateMethod.toString()
if (disallowedDateMethods.has(dateMethodString)) {
context.report({
node,
data: {
method: dateMethod,
suggestion: formatSuggestion(
dateMethodString,
methodToDateFns[dateMethodString]
),
},
messageId: 'banDateMutation',
})
}
}
},
}
},
})
5 changes: 5 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import banDateMutation from './ban-date-mutation'

export default {
'ban-date-mutation': banDateMutation,
}
12 changes: 12 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ESLintUtils } from '@typescript-eslint/experimental-utils'

const { getParserServices } = ESLintUtils

const version = require('../package.json').version

export const createRule = ESLintUtils.RuleCreator(
(name) =>
`https://github.com/chdsbd/eslint-plugin-better-dates/blob/v${version}/packages/eslint-plugin/docs/rules/${name}.md`
)

export { getParserServices }
Loading

0 comments on commit e95d9a3

Please sign in to comment.