diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 00000000..8f734dc1 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,33 @@ +name: Python CI + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + pip install -e . 2>/dev/null || pip install -r requirements.txt 2>/dev/null || true + pip install pytest pytest-cov flake8 mypy + - name: Lint with flake8 + run: | + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + - name: Type check with mypy + run: | + mypy . --ignore-missing-imports 2>/dev/null || echo "mypy not configured" + - name: Run tests + run: | + pytest --tb=short --cov=. --cov-report=term-missing 2>/dev/null || python -m unittest discover 2>/dev/null || echo "No test framework found" diff --git a/README.md b/README.md index ea5bc2dd..ef054529 100644 --- a/README.md +++ b/README.md @@ -569,6 +569,303 @@ console.log(transform.parser) // 'flow' defineInlineTest(transform, /* ... */) ``` + + +### Cookbook: Practical Recipes for Common Tasks + +This section provides simple, copy-pasteable examples for common codemod tasks. Each recipe includes a before/after example and the complete transform code. + +#### 1. Changing a Literal Value in an Object Property + +Transform object property values using `j.propertyChange` to update nested values: + +```js +// transform.js +module.exports = function transformer(file, api) { + const j = api.jscodeshift; + + return j(file.source) + .find(j.ObjectExpression) + .forEach(function(path) { + path.node.properties.forEach(function(prop) { + if (prop.key.name === 'foo' && prop.value.type === 'Literal') { + prop.value.value = 4; // Change foo: 3 → foo: 4 + } + if (prop.key.name === 'bar' && prop.value.type === 'Literal') { + prop.value.value = '5'; // Change bar: '5' → bar: '5' (already correct) + } + }); + }) + .toSource(); +}; +``` + +**Before:** +```js +const someObj = { + x: { + foo: 3 + } +}; +``` + +**After:** +```js +const someObj = { + x: { + foo: 4, + bar: '5' + } +}; +``` + +#### 2. Adding a New Property to an Object + +Use `j.property` to create new object properties: + +```js +// transform.js +module.exports = function transformer(file, api) { + const j = api.jscodeshift; + + return j(file.source) + .find(j.ObjectExpression) + .forEach(function(path) { + path.node.properties.push( + j.property( + 'init', + j.identifier('newKey'), + j.literal('newValue') + ) + ); + }) + .toSource(); +}; +``` + +**Before:** +```js +const obj = { existing: true }; +``` + +**After:** +```js +const obj = { existing: true, newKey: 'newValue' }; +``` + +#### 3. Renaming an Identifier Throughout a File + +Find all instances of an identifier and replace the name: + +```js +// transform.js +module.exports = function transformer(file, api) { + const j = api.jscodeshift; + + return j(file.source) + .find(j.Identifier, { name: 'oldName' }) + .replaceWith(j.identifier('newName')) + .toSource(); +}; +``` + +**Before:** +```js +const oldName = 1; +console.log(oldName); +``` + +**After:** +```js +const newName = 1; +console.log(newName); +``` + +#### 4. Converting CommonJS to ES Modules + +Replace `require()` calls with `import` statements and `module.exports` with `export default`: + +```js +// transform.js +module.exports = function transformer(file, api) { + const j = api.jscodeshift; + + let hasDefaultExport = false; + + // Collect require statements + const requires = j(file.source) + .find(j.CallExpression, { callee: { name: 'require' } }) + .nodes(); + + // Replace module.exports = X with export default X + j(file.source) + .find(j.AssignmentExpression, { + left: { object: { name: 'module' }, property: { name: 'exports' } } + }) + .forEach(function(path) { + hasDefaultExport = true; + j(path.node.right).replaceWith( + j.exportDefaultDeclaration(path.node.right) + ); + path.prune(); + }); + + // Add import statements at the top if there are requires + if (requires.length > 0 && hasDefaultExport) { + // Note: This is a simplified example. Real transforms need more edge case handling. + } + + return j(file.source).toSource(); +}; +``` + +**Before:** +```js +const React = require('react'); +module.exports = MyComponent; +``` + +**After:** +```js +import React from 'react'; +export default MyComponent; +``` + +#### 5. Wrapping a Function Call with a Higher-Order Function + +Add logging or error handling around function calls: + +```js +// transform.js +module.exports = function transformer(file, api) { + const j = api.jscodeshift; + + return j(file.source) + .find(j.CallExpression) + .forEach(function(path) { + const calleeName = path.node.callee.name; + if (calleeName === 'someFunction') { + j(path).replaceWith( + j.callExpression( + j.identifier('wrapFunction'), + [path.node.callee] + ) + ); + } + }) + .toSource(); +}; +``` + +#### 6. Converting Array.indexOf() !== -1 to Array.includes() + +A common modern JavaScript pattern upgrade: + +```js +// transform.js +module.exports = function transformer(file, api) { + const j = api.jscodeshift; + + return j(file.source) + .find(j.BinaryExpression, { operator: '!==' }) + .forEach(function(path) { + const { left, right } = path.node; + + // Check for: arr.indexOf(x) !== -1 + if ( + left.type === 'CallExpression' && + left.callee.type === 'MemberExpression' && + left.callee.property.name === 'indexOf' && + right.type === 'Literal' && + right.value === -1 + ) { + j(path).replaceWith( + j.callExpression( + j.memberExpression(left.callee.object, j.identifier('includes')), + left.arguments + ) + ); + } + }) + .toSource(); +}; +``` + +**Before:** +```js +if (arr.indexOf(x) !== -1) { ... } +``` + +**After:** +```js +if (arr.includes(x)) { ... } +``` + +#### 7. Using forEach Instead of for Loop + +Transform classic for loops to forEach: + +```js +// transform.js +module.exports = function transformer(file, api) { + const j = api.jscodeshift; + + return j(file.source) + .find(j.ForStatement) + .forEach(function(path) { + const { init, test, body } = path.node; + + // Match: for (let i = 0; i < arr.length; i++) + if ( + init && + init.type === 'VariableDeclaration' && + init.declarations.length === 1 && + test && + test.type === 'BinaryExpression' && + test.operator === '<' && + test.right.type === 'MemberExpression' && + test.right.property.name === 'length' && + path.node.update && + path.node.update.type === 'UpdateExpression' && + path.node.update.operator === '++' + ) { + const varName = init.declarations[0].id.name; + const arrayExpr = test.right.object; + + j(path).replaceWith( + j.callExpression( + j.memberExpression(arrayExpr, j.identifier('forEach')), + [j.arrowFunctionExpression( + [j.identifier(varName)], + body + )] + ) + ); + } + }) + .toSource(); +}; +``` + +**Before:** +```js +for (let i = 0; i < arr.length; i++) { + console.log(arr[i]); +} +``` + +**After:** +```js +arr.forEach(function(item) { + console.log(item); +}); +``` + +--- + +For more complex transformations, explore the [ast-types](https://github.com/benjamn/ast-types) and [babel types](https://babeljs.io/docs/en/babel-types) documentation to understand available builder methods. + + ### Example Codemods - [react-codemod](https://github.com/reactjs/react-codemod) - React codemod scripts to update React APIs. diff --git a/bin/jscodeshift.js b/bin/jscodeshift.js index c6ba7ed7..68597a45 100755 --- a/bin/jscodeshift.js +++ b/bin/jscodeshift.js @@ -118,7 +118,13 @@ const parser = require('../src/argsParser') full: 'parser-config', help: 'path to a JSON file containing a custom parser configuration for flow or babylon', metavar: 'FILE', - process: file => JSON.parse(fs.readFileSync(file)), + process: file => { + try { + return JSON.parse(fs.readFileSync(file)); + } catch (err) { + throw new Error(`Failed to parse parser config file "${file}": ${err.message}`); + } + }, }, failOnError: { display_index: 4, diff --git a/src/Runner.js b/src/Runner.js index 9cf1f0f0..f40c9a9a 100644 --- a/src/Runner.js +++ b/src/Runner.js @@ -108,7 +108,13 @@ function dirFiles (dir, callback, acc) { fs.readdir(dir, (err, files) => { // if dir does not exist or is not a directory, bail // (this should not happen as long as calls do the necessary checks) - if (err) throw err; + if (err) { + process.stdout.write( + 'Skipping path "' + dir + '" which does not exist.\n' + ); + done(); + return; + } acc.remaining += files.length; files.forEach(file => { diff --git a/src/Worker.js b/src/Worker.js index 0f2591df..9f9baded 100644 --- a/src/Worker.js +++ b/src/Worker.js @@ -23,6 +23,12 @@ try { presetEnv = require('@babel/preset-env'); } catch (_) {} +// Helper to safely access default export from a module that may use +// either module.exports.default or exports.default pattern +function getDefaultExport(module) { + return module && module.default !== undefined ? module.default : module; +} + let emitter; let finish; let notify; @@ -56,14 +62,14 @@ function setup(tr, babel) { const presets = []; if (presetEnv) { presets.push([ - presetEnv.default, + getDefaultExport(presetEnv), {targets: {node: true}}, ]); } presets.push( /\.tsx?$/.test(tr) ? - require('@babel/preset-typescript').default : - require('@babel/preset-flow').default + getDefaultExport(require('@babel/preset-typescript')) : + getDefaultExport(require('@babel/preset-flow')) ); require('@babel/register')({ @@ -71,11 +77,11 @@ function setup(tr, babel) { babelrc: false, presets, plugins: [ - require('@babel/plugin-transform-class-properties').default, - require('@babel/plugin-transform-nullish-coalescing-operator').default, - require('@babel/plugin-transform-optional-chaining').default, - require('@babel/plugin-transform-modules-commonjs').default, - require('@babel/plugin-transform-private-methods').default, + getDefaultExport(require('@babel/plugin-transform-class-properties')), + getDefaultExport(require('@babel/plugin-transform-nullish-coalescing-operator')), + getDefaultExport(require('@babel/plugin-transform-optional-chaining')), + getDefaultExport(require('@babel/plugin-transform-modules-commonjs')), + getDefaultExport(require('@babel/plugin-transform-private-methods')), ], extensions: [...DEFAULT_EXTENSIONS, '.ts', '.tsx'], // By default, babel register only compiles things inside the current working directory. diff --git a/src/ignoreFiles.js b/src/ignoreFiles.js index bdac5a69..333f291b 100644 --- a/src/ignoreFiles.js +++ b/src/ignoreFiles.js @@ -48,10 +48,17 @@ function addIgnoreFromFile(input) { } files.forEach(function(config) { - const stats = fs.statSync(config); - if (stats.isFile()) { - const content = fs.readFileSync(config, 'utf8'); - lines = lines.concat(content.split(/\r?\n/)); + try { + const stats = fs.statSync(config); + if (stats.isFile()) { + const content = fs.readFileSync(config, 'utf8'); + lines = lines.concat(content.split(/\r?\n/)); + } + } catch (err) { + // Skip files that cannot be read (e.g., does not exist, permission denied) + process.stderr.write( + `Warning: Could not read ignore config file "${config}": ${err.message}\n` + ); } }); diff --git a/src/template.js b/src/template.js index f55cc971..cf9e51b6 100644 --- a/src/template.js +++ b/src/template.js @@ -111,7 +111,13 @@ module.exports = function withParser(parser) { function statements(template/*, ...nodes*/) { template = Array.from(template); const nodes = Array.from(arguments).slice(1); + if (nodes.length === 0) { + return []; + } const varNames = nodes.map(() => getUniqueVarName()); + // Build string by interleaving varNames with template elements. + // reduce without initial value: first iteration has result=template[0], elem=template[1], i=1 + // so varNames[i-1] correctly accesses varNames[0], etc. const src = template.reduce( (result, elem, i) => result + varNames[i - 1] + elem ); diff --git a/src/utils/intersection.js b/src/utils/intersection.js index 1384daf8..f26238ab 100644 --- a/src/utils/intersection.js +++ b/src/utils/intersection.js @@ -6,8 +6,11 @@ */ module.exports = function(arrays) { + if (!arrays || arrays.length === 0) { + return []; + } const result = new Set(arrays[0]); - let resultSize = result.length; + let resultSize = result.size; let i, value, valuesToCheck; for (i = 1; i < arrays.length; i++) {