tunnckoCore / opensource

Delivering delightful digital solutions. Monorepo of monorepos of Open Source packages with combined ~100M/month downloads, semantically versioned following @conventional-commits. Fully powered ES Modules, @Airbnb @ESLint + @Prettier, independent & fixed versioning. Quality with @Actions, CodeQL, & Dependabot.
https://tunnckocore.com/opensource
480 stars 18 forks source link

parse-function: guide/plugin for es6 class constructors #135

Open FranciscoG opened 4 years ago

FranciscoG commented 4 years ago

Support plan

Context

What are you trying to achieve or the steps to reproduce?

When running app.parse on an es6 Class declaration that uses a constructor function, the results return an empty args array.

const parseFunction = require('parse-function');

const app = parseFunction(); // even passing `{ecmaVersion: 2017}` does not change the results

class MyClass {
  constructor(param1, param2) {
    this.something = param1;
    this.another = param2;
  }

  doSomething() {
    console.log(this.something);
  }
}

const result = app.parse(MyClass);
console.log(result)

What was the result you got?

{
  name: null,
  body: '',
  args: [],
  params: '',
  defaults: {},
  value: 'class MyClass {\n' +
    '  constructor(param1, param2) {\n' +
    '    this.something = param1;\n' +
    '    this.another = param2;\n' +
    '  }\n' +
    '\n' +
    '  doSomething() {\n' +
    '    console.log(this.something);\n' +
    '  }\n' +
    '}',
  isValid: true,
  isArrow: false,
  isAsync: false,
  isNamed: false,
  isAnonymous: false,
  isGenerator: false,

What result did you expect?

I expected args to be ['param1', 'param2']

auto-comment[bot] commented 4 years ago

Thank you for raising this issue! We will try and get back to you as soon as possible. Please make sure you format it properly, followed our code of conduct, and have given us as much context as possible. Hey @tunnckoCore, check out this one too! ;)

FranciscoG commented 4 years ago

ok so i was able to create a plugin that handles this just in case anyone needs this right away.

app.use(app => (node, result) => {
  if (node.type === "ClassExpression") {
    const nodeConstructor = node.body.body.find(b => b.kind === "constructor");
    if (nodeConstructor) {
      result.constructorArgs = nodeConstructor.params.map(n => n.name);
      result.constructorParams = nodeConstructor.params.map(n => n.name).join(', ');
    }
  }
  return result;
});

and now my results look like this

{
  name: null,
  body: '',
  args: [],
  params: '',
  defaults: {},
  value: 'class MyClass {\n' +
    '  constructor(param1, param2) {\n' +
    '    this.something = param1;\n' +
    '    this.another = param2;\n' +
    '  }\n' +
    '\n' +
    '  doSomething() {\n' +
    '    console.log(this.something);\n' +
    '  }\n' +
    '}',
  isValid: true,
  isArrow: false,
  isAsync: false,
  isNamed: false,
  isAnonymous: false,
  isGenerator: false,
  isExpression: false,
  constructorArgs: [ 'param1', 'param2' ],
  constructorParams: 'param1, param2'
}

edit: this is not complete, it will not handle parameters with defaults, but it's a start

tunnckoCore commented 4 years ago

Great report and thanks for the plugin. That's the beauty of plugins - anyone can fix his problems or find a temporary solution to them, almost immediately if he want.

I guess, it's almost expected that it doesn't return what you expect. We completely depend on parseExpression, additionally you can pass custom options.parse function too. The classes are not exactly a function. If you pass the class constructor directly it would probably work just fine, like so

parse(myClass.constructor)
parse(MyClass.prototype.constructor)

Also I think that parse(MyClass) isn't immediately explicit too, which is almost always the better thing.

tunnckoCore commented 4 years ago

edit: this is not complete, it will not handle parameters with defaults, but it's a start

We can also import the rest of the plugins manually and pass nodeConstructor to them.

FranciscoG commented 4 years ago

I forgot to mention in my original post that I had also tried const result = app.parse(MyClass.prototype.constructor) and it gave me the same results as const result = app.parse(MyClass)

I really do appreciate the extensibility the plugin system adds so thank you for that! I've got it working for my needs right now.

This is what I'm using. I've added handling for parameters that have default values and for my use case I've separate those parameters from the other ones. Might not be the best solution for everyone but works for me.

function getDefaultParams(paramNodes = []) {
  return paramNodes
    .filter((n) => n.type === "AssignmentPattern")
    .map((d) => {
      return {
        param: d.left.name,
        value: d.right.value,
      };
    });
}

app.use(app => (node, result) => {
  result.constructor = {
    args: [],
    argsWithDefaults: []
  };
  if (node.type === "ClassExpression") {
    const nodeConstructor = node.body.body.find(n => n.kind === "constructor");

    if (nodeConstructor) {
      result.constructor.argsWithDefaults = getDefaultParams(
        nodeConstructor.params
      );
      result.constructor.args = nodeConstructor.params
        .filter(p => p.type === "Identifier")
        .map(n => n.name);
    }
  }
  return result;
});

my example constructor now looks like this: constructor(param1, param2, param3 = "test")

and this is the result the above plugin produces. I'm not touching any of the existing properties, I'm only adding my own new property

{ 
 // ... 
 constructor: {
     args: [
      "param1",
      "param2"
    ],
    argsWithDefaults: [
      {
        param: "param3",   
        value: "test"
      }
    ]
  }
}
tunnckoCore commented 4 years ago

@FranciscoG awesome! :heart_eyes_cat: