-
-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: introduce eslint plugin #121
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# @codeshift/eslint-plugin | ||
|
||
Placeholder |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { RuleTester } from 'eslint'; | ||
|
||
import rule from '../rules/no-codemod-comment'; | ||
|
||
const ruleTester = new RuleTester({ | ||
parserOptions: { | ||
ecmaVersion: 'latest', | ||
ecmaFeatures: { | ||
jsx: true, | ||
}, | ||
}, | ||
}); | ||
|
||
ruleTester.run('no-codemod-comment', rule, { | ||
valid: [ | ||
{ | ||
code: [ | ||
// This has an invalid header so is ignored | ||
`/* integrity: codemod-hash-1399692252 */`, | ||
`// TODO codemod-generated-comment signed: codemod-hash-1399692252`, | ||
`<Hello />`, | ||
].join('\n'), | ||
}, | ||
{ | ||
code: [`<Hello />`].join('\n'), | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
code: [ | ||
`/* AUTOGENERATED CODEMOD SIGNATURE signed: */`, | ||
`// TODO: This is a codemod generated comment.`, | ||
`<Hello>`, | ||
` <AKModal open={false} />`, | ||
`</Hello>`, | ||
].join('\n'), | ||
errors: [{ messageId: 'noHashMatch' }], | ||
}, | ||
{ | ||
code: [ | ||
`/* AUTOGENERATED CODEMOD SIGNATURE signed: codemod-hash-1510141432 */`, | ||
`// TODO: This is a codemod generated comment.`, | ||
`<Hello />`, | ||
].join('\n'), | ||
errors: [{ messageId: 'noHashMatch' }], | ||
}, | ||
{ | ||
code: [ | ||
`/* AUTOGENERATED CODEMOD SIGNATURE signed: codemod-hash-524539434,codemod-hash-2056612822 */`, | ||
`// TODO: This is a codemod generated comment.`, | ||
`const y = <Hello />`, | ||
`// TODO: This is a codemod generated comment. Another comment`, | ||
`const x = <Hello />`, | ||
].join('\n'), | ||
errors: [{ messageId: 'noHashInSource' }, { messageId: 'noHashMatch' }], | ||
}, | ||
{ | ||
code: [ | ||
`/* AUTOGENERATED CODEMOD SIGNATURE signed: codemod-hash-524539434,codemod-hash-2056612820 */`, | ||
`// TODO: This is a codemod generated comment.`, | ||
`const y = <Hello />`, | ||
`// TODO: This is a codemod generated comment. Another comment`, | ||
`const x = <Hello />`, | ||
].join('\n'), | ||
errors: [ | ||
{ messageId: 'noHashInSource' }, | ||
{ messageId: 'noHashInSource' }, | ||
], | ||
}, | ||
], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import { RuleTester } from 'eslint'; | ||
|
||
import rule, { UpdatePropNameOptions } from '../rules/rename-prop'; | ||
|
||
const ruleTester = new RuleTester({ | ||
parserOptions: { | ||
sourceType: 'module', | ||
ecmaVersion: 'latest', | ||
ecmaFeatures: { | ||
jsx: true, | ||
}, | ||
}, | ||
}); | ||
|
||
ruleTester.run('jsx/update-prop-name', rule, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do you see Or maybe would they have multiple configurations setup, one for modal-dialog, one for button etc then they remain in the eslintfile and can be updated as the version changes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think yeah it could run alongside / with / in place of / any codemod approach. Maybe a codemod could even be used to update the eslint config around releases. The flexibility of the current config is that it can be used with lots of configs and thus different modules. The difficult part to figure out is when to turn it off or how to manage ratcheting it. |
||
valid: [ | ||
{ | ||
options: [ | ||
{ | ||
source: '@atlaskit/modal-dialog', | ||
specifier: 'Modal', | ||
oldProp: 'open', | ||
newProp: 'isOpen', | ||
}, | ||
] as UpdatePropNameOptions[], | ||
code: ` | ||
import { Modal as AKModal } from '@atlaskit/modal-dialog' | ||
|
||
const App = () => ( | ||
<Hello> | ||
<AKModal isOpen={false} /> | ||
</Hello> | ||
) | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
const App = () => ( | ||
<Hello> | ||
<AKModal open={false} /> | ||
</Hello> | ||
) | ||
`, | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
options: [ | ||
{ | ||
source: '@atlaskit/modal-dialog', | ||
specifier: 'Modal', | ||
oldProp: 'open', | ||
newProp: 'isOpen', | ||
}, | ||
] as UpdatePropNameOptions[], | ||
code: ` | ||
import { Modal } from '@atlaskit/modal-dialog' | ||
|
||
const App = () => ( | ||
<Hello> | ||
<Modal open={false} /> | ||
</Hello> | ||
) | ||
`, | ||
errors: [{ messageId: 'renameProp' }], | ||
output: ` | ||
import { Modal } from '@atlaskit/modal-dialog' | ||
|
||
const App = () => ( | ||
<Hello> | ||
<Modal isOpen={false} /> | ||
</Hello> | ||
) | ||
`, | ||
}, | ||
{ | ||
options: [ | ||
{ | ||
source: '@atlaskit/modal-dialog', | ||
specifier: 'Modal', | ||
oldProp: 'open', | ||
newProp: 'isOpen', | ||
}, | ||
] as UpdatePropNameOptions[], | ||
code: ` | ||
import { Modal as AKModal } from '@atlaskit/modal-dialog' | ||
|
||
const App = () => ( | ||
<Hello> | ||
<AKModal open={false} /> | ||
</Hello> | ||
) | ||
`, | ||
errors: [{ messageId: 'renameProp' }], | ||
output: ` | ||
import { Modal as AKModal } from '@atlaskit/modal-dialog' | ||
|
||
const App = () => ( | ||
<Hello> | ||
<AKModal isOpen={false} /> | ||
</Hello> | ||
) | ||
`, | ||
}, | ||
{ | ||
options: [ | ||
{ | ||
source: '@example/thing', | ||
specifier: 'Checkbox', | ||
oldProp: 'selected', | ||
newProp: 'checked', | ||
}, | ||
] as UpdatePropNameOptions[], | ||
code: ` | ||
import { Checkbox } from '@example/thing' | ||
|
||
const App = () => ( | ||
<Hello> | ||
<Checkbox selected={false} /> | ||
</Hello> | ||
) | ||
`, | ||
errors: [{ messageId: 'renameProp' }], | ||
output: ` | ||
import { Checkbox } from '@example/thing' | ||
|
||
const App = () => ( | ||
<Hello> | ||
<Checkbox checked={false} /> | ||
</Hello> | ||
) | ||
`, | ||
}, | ||
{ | ||
options: [ | ||
{ | ||
source: '@example/thing', | ||
specifier: 'default', | ||
oldProp: 'selected', | ||
newProp: 'checked', | ||
}, | ||
] as UpdatePropNameOptions[], | ||
code: ` | ||
import Checkbox from '@example/thing' | ||
|
||
const App = () => ( | ||
<Hello> | ||
<Checkbox selected={false} /> | ||
</Hello> | ||
) | ||
`, | ||
errors: [{ messageId: 'renameProp' }], | ||
output: ` | ||
import Checkbox from '@example/thing' | ||
|
||
const App = () => ( | ||
<Hello> | ||
<Checkbox checked={false} /> | ||
</Hello> | ||
) | ||
`, | ||
}, | ||
], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/** | ||
* | ||
* Example hash format | ||
* @example | ||
* ```ts | ||
* // TODO: This file has been altered by a codemod. integrity: codemod-hash-5131781 | ||
* ``` | ||
*/ | ||
export function hash(str: string) { | ||
let hashValue = 0; | ||
for (let i = 0; i < str.length; i++) { | ||
const char = str.charCodeAt(i); | ||
hashValue = (hashValue << 5) - hashValue + char; | ||
hashValue = hashValue & hashValue; // Convert to 32bit integer | ||
} | ||
return `codemod-hash-${Math.abs(hashValue)}`; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import renameProp from './rules/rename-prop'; | ||
import noCodemodComment from './rules/no-codemod-comment'; | ||
|
||
export const rules = { | ||
/** | ||
* Remove or update a jsx prop | ||
*/ | ||
'jsx/update-prop-name': renameProp, | ||
/** | ||
* Remove or update import | ||
*/ | ||
'update-import': renameProp, | ||
/** | ||
* Has codemod TODO | ||
*/ | ||
'no-codemod-comment': noCodemodComment, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import type { Rule } from 'eslint'; | ||
import { hash } from '../hash'; | ||
|
||
const SIGNATURE_HEADER = 'AUTOGENERATED CODEMOD SIGNATURE'; | ||
const TODO_COMMENT = 'TODO: This is a codemod generated comment.'; | ||
const HEADER_REGEX = /(codemod-hash-\d+)/g; | ||
|
||
/** | ||
* If there is the presence of a header we then check all comments to verify if they have matching hashes with the header | ||
*/ | ||
const rule: Rule.RuleModule = { | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Errors if a block has a codemod generated comment in it', | ||
recommended: true, | ||
}, | ||
messages: { | ||
noHashInSource: | ||
'The file {{ file }} includes a comment generated by a codemod. This comment requires further manual verification.', | ||
noHashMatch: | ||
'The file {{ file }} includes a comment generated by a codemod but its hash <{{expectedHashValue}}> does not match the header <{{currentHashValue}}>. Please rerun the codemod, or if the codemod changes are now verified - remove the comment and header from the file.', | ||
}, | ||
}, | ||
create(context) { | ||
const filename = context.getFilename(); | ||
const source = context.getSourceCode(); | ||
const comments = source.getAllComments(); | ||
const headerComment = comments.find(comment => | ||
comment.value.includes(SIGNATURE_HEADER), | ||
); | ||
|
||
return { | ||
Program() { | ||
if (!headerComment) { | ||
return; | ||
} | ||
|
||
const headerSignatureMatches = Array.from( | ||
headerComment.value.matchAll(HEADER_REGEX), | ||
); | ||
const codemodComments = comments.filter(comment => | ||
comment.value.includes(TODO_COMMENT), | ||
); | ||
|
||
codemodComments.forEach((com, index) => { | ||
const currentHashValue = hash(com.value); | ||
const expectedHashValue = headerSignatureMatches[index] | ||
? headerSignatureMatches[index][0] | ||
: ''; | ||
|
||
if (currentHashValue !== expectedHashValue) { | ||
context.report({ | ||
loc: com.loc!, | ||
messageId: 'noHashMatch', | ||
data: { | ||
file: filename, | ||
expectedHashValue, | ||
currentHashValue, | ||
}, | ||
}); | ||
} else { | ||
context.report({ | ||
loc: com.loc!, | ||
messageId: 'noHashInSource', | ||
data: { | ||
file: filename, | ||
}, | ||
}); | ||
} | ||
}); | ||
}, | ||
}; | ||
}, | ||
}; | ||
|
||
export default rule; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain how the integrity check works?
So if I was to imagine a codemod run on a repo and the diffs raised in a PR, would these be presented as an error? Or can they be merged and subsequent changes to the file would trigger an error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danieldelcore I should add a README for these rules sorry mate, will amend.
In essence the idea is that a codemod would add a header comment and generate hashes off the comments it introduces to code. Each of these comments are validated against the header. So it's clear that this were generated by a script.
I thought of multiple different approaches here tho for example:
Or some combination of the above.